这里的T extends object
与U extends keyof T
都是泛型约束,分别将T约束为对象类型和将U约束为T键名的字面量联合类型。我们通常使用泛型约束来**“使得泛型收窄”**。以一个使用条件类型作为函数返回值类型的例子:
declare function strOrnum(
x: T
): T extends true ? string : number;
在这种情况下,条件类型的推导就会被延迟(deferred),因为此时类型系统没有足够的信息来完成判断。只有给出了所需信息(在这里是x值),才可以完成推导。
const strReturnType = strOrNum(true);
const numReturnType = strOrNum(false);
同样的,就像三元表达式可以嵌套,条件类型也可以嵌套,如果你看过一些框架源码,也会发现其中存在着许多嵌套的条件类型,无他,条件类型可以将类型约束收拢到非常精确的范围内。
type TypeName = T extends string
? “string”
: T extends number
? “number”
: T extends boolean
? “boolean”
: T extends undefined
? “undefined”
: T extends Function
? “function”
: “object”;
分布式条件类型 Distributive Conditional Types
官方文档对分布式条件类型的讲解内容甚至要多于条件类型,因此你也知道这玩意没那么简单了吧~ 分布式条件类型实际上不是一种特殊的条件类型,而是其特性之一。概括地说,就是 对于属于裸类型参数的检查类型,条件类型会在实例化时期自动分发到联合类型上
原文: Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation
先提取几个关键词,然后我们再通过例子理清这个概念:
-
裸类型参数
-
实例化
-
分发到联合类型
// 使用上面的TypeName类型别名
// “string” | “function”
type T1 = TypeName<string | (() => void)>
// “string” | “object”
type T2 = TypeName<string | string[]>
// “object”
type T3 = TypeName<string[] | number[]>
我们发现在上面的例子里,条件类型的推导结果都是联合类型(T3实际上也是,只不过相同所以被合并了),并且就是类型参数被依次进行条件判断的结果。是不是get到了一点什么?我们再看另一个例子:
type Naked = T extends boolean ? “Y” : “N”;
type Wrapped = [T] extends [boolean] ? “Y” : “N”;
/*
* 先分发到 Naked | Naked
* 然后到 “N” | “Y”
*/
type Distributed = Naked<number | boolean>;
/*
* 不会分发 直接是 [number | boolean] extends [boolean]
* 然后是"N"
*/
type NotDistributed = Wrapped<number | boolean>;
现在我们可以来讲讲这几个概念了:
-
裸类型参数,没有额外被接口/类型别名包裹过的,就像被
Wrapped
包裹后就不能再被称为裸类型参数。 -
实例化,其实就是条件类型的判断过程,在这里两个例子的实例化过程实际上是不同的,具体会在下一点中介绍。
-
分发至联合类型的过程:
-
- 对于TypeName,它内部的类型参数T是没有被包裹过的,所以
TypeName<string | (() => void)>
会被分发为TypeName<string> | TypeName<(() => void)>
,然后再次进行判断,最后分发为"string" | "function"
。
- 对于TypeName,它内部的类型参数T是没有被包裹过的,所以
-
抽象下具体过程:```typescript ( A | B | C ) extends T ? X : Y // 相当于 (A extends T ? X : Y) | (B extends T ? X : Y) | (B extends T ? X : Y)
复制代码
一句话概括:没有被额外包装的联合类型参数,在条件类型进行判定时会将联合类型分发,分别进行判断。
infer关键字
infer
是inference
的缩写,通常的使用方式是infer R
,R
表示 待推断的类型。通常infer
不会被直接使用,而是被放置在底层工具类型中,需要在条件类型中使用。看一个简单的例子,用于获取函数返回值类型的工具类型ReturnType
:
const foo = (): string => {
return “linbudu”;
};
// string
type FooReturnType = ReturnType;
infer
的使用思路可能不是那么好习惯,我们可以用前端开发中常见的一个例子类比,页面初始化时先显示占位交互,像Loading/骨架屏,在请求返回后再去渲染真实数据。infer
也是这个思路,类型系统在获得足够的信息后,就能将infer后跟随的类型参数推导出来,最后返回这个推导结果。
type ReturnType = T extends (…args: any[]) => infer R ? R : any;
类似的,借着这个思路我们还可以获得函数入参类型、类的构造函数入参类型、Promise内部的类型等,这些工具类型我们会在后面讲到。infer其实没有特别难消化的知识点,它需要的只是思路的转变,你要理解 延迟推断 的概念。
类型守卫 与 is in关键字 Type Guards
前面的内容可能不是那么符合人类直觉,需要一点时间消化,这一节我们来看点简单(相对)且直观的知识点:类型守卫。假设有这么一个字段,它可能字符串也可能是数字:
numOrStrProp: number | string;
现在在使用时,你想将这个字段的联合类型缩小范围,比如精确到string
,你可能会这么写:
export const isString = (arg: unknown): boolean =>
typeof arg === “string”;
看看这么写的效果:
function useIt(numOrStr: number | string) {
if (isString(numOrStr)) {
console.log(numOrStr.length);
}
}
啊哦,看起来isString
函数并没有起到缩小类型范围的作用,参数依然是联合类型。这个时候就该使用is
关键字了:
export const isString = (arg: unknown): arg is string =>
typeof arg === “string”;
这个时候再去使用,就会发现在isString(numOrStr)
为true后,numOrStr
的类型就被缩小到了string
。这只是以原始类型为成员的联合类型,我们完全可以扩展到各种场景上,先看一个简单的假值判断:
export type Falsy = false | “” | 0 | null | undefined;
export const isFalsy = (val: unknown): val is Falsy => !val;
是不是还挺有用?这应该是我日常用的最多的类型别名之一了。也可以在in关键字的加持下,进行更强力的类型判断,思考下面这个例子,要如何将 " A | B " 的联合类型缩小到"A"?
class A {
public a() {}
public useA() {
return “A”;
}
}
class B {
public b() {}
public useB() {
return “B”;
}
}
再联想下for...in
循环,它遍历对象的属性名,而in
关键字也是一样:
function useIt(arg: A | B): void {
if (“a” in arg) {
arg.useA();
} else {
arg.useB();
}
}
再看一个使用字面量类型作为类型守卫的例子:
interface IBoy {
name: “mike”;
gf: string;
}
interface IGirl {
name: “sofia”;
bf: string;
}
function getLover(child: IBoy | IGirl): string {
if (child.name === “mike”) {
return child.gf;
} else {
return child.bf;
}
}
之前有个小哥问过一个问题,我想很多用TS写接口的小伙伴可能都遇到过,即登录与未登录下的用户信息是完全不同的接口:
interface IUserProps {
isLogin: boolean;
name: string; // 用户名称仅在登录时有
from: string; // 用户来源(一般用于埋点),仅在未登录时有
}
这种时候使用字面量类型守卫:
function getUserInfo(user: IUnLogin | ILogined): string {
return user.isLogin ? user.id : user.from;
}
还可以使用instanceof
来进行实例的类型守卫,建议聪明的你动手尝试下~
工具类型Tool Type
这一章是本文的最后一部分,应该也是本文“性价比”最高的一部分了,因为即使你还是不太懂这些工具类型的底层实现,也不影响你把它用好。就像Lodash不会要求你每用一个函数就熟知原理一样。这一部分包括TS内置工具类型与社区的扩展工具类型,我个人推荐在完成学习后记录你觉得比较有价值的工具类型,并在自己的项目里新建一个.d.ts
文件存储它。
在继续阅读前,请确保你掌握了上面的知识,它们是类型编程的基础
内置工具类型
在上面我们已经实现了内置工具类型中被使用最多的一个:
type Partial = {
[K in keyof T]?: T[k];
};
它用于将一个接口中的字段变为全部可选,除了映射类型以外,它只使用了?
可选修饰符,那么我现在直接掏出小抄(好家伙):
-
去除可选修饰符:
-?
-
只读修饰符:
readonly
-
去除只读修饰符:
-readonly
恭喜,你得到了Required
和Readonly
(去除readonly修饰符的工具类型不属于内置的,我们会在后面看到):
type Required = {
[K in keyof T]-?: T[K];
};
type Readonly = {
readonly [K in keyof T]: T[K];
};
在上面我们实现了一个pick函数:
function pick<T extends object, U extends keyof T>(obj: T, keys: U[]): T[U][] {
return keys.map((key) => obj[key]);
}
照着这种思路,假设我们现在需要从一个接口中挑选一些字段:
type Pick<T, K extends keyof T> = {
};
// 期望用法
type Part = Pick<A, “a” | “b”>
还是映射类型,只不过现在映射类型的映射源是类型参数K
。既然有了Pick
,那么自然要有Omit
,它和Pick
的写法非常像,但有一个问题要解决:我们要怎么表示T
中剔除了K
后的剩余字段?
Pick选取传入的键值,Omit移除传入的键值
这里我们又要引入一个知识点:never
类型,它表示永远不会出现的类型,通常被用来将收窄联合类型或是接口,详细可以看 [尤大的知乎回答](<https://www.zhihu.com/search?type=content&q=ts never>), 在这里 我们不做展开介绍。上面的场景其实可以简化为:
// “3” | “4” | “5”
type LeftFields = Exclude<“1” | “2” | “3” | “4” | “5”, “1” | “2”>;
可以用排列组合的思路考虑:"1"
在"1" | "2"
里面吗("1" extends "1"|"2" -> true
)?在啊, 那让它爬,"3"在吗?不在那就让它留下来。这里实际上使用到了分布式条件类型的特性,假设Exclude接收T U两个类型参数,T联合类型中的类型会依次与U类型进行判断,如果这个类型参数在U中,就剔除掉它(赋值为never)
type Exclude<T, U> = T extends U ? never : T;
那么Omit:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
剧透下,几乎所有使用条件类型的场景,把判断后的赋值语句反一下,就会有新的场景,比如Exclude
移除掉键名,那反一下就是保留键名:
type Extract<T, U> = T extends U ? T : never;
再来看个常用的工具类型Record<Keys, Type>
,通常用于生成以联合类型为键名(Keys
),键值类型为Type
的新接口,比如:
type MyNav = “a” | “b” | “b”;
interface INavWidgets {
widgets: string[];
title?: string;
keepAlive?: boolean;
}
const router: Record<MyNav, INavWidgets> = {
a: { widget: [‘’] },
b: { widget: [‘’] },
c: { widget: [‘’] },
}
其实很简单,把Keys
的每个键值拿出来,类型规定为Type
即可
// K extends keyof any 约束K必须为联合类型
type Record<K extends keyof any, T> = {
};
在前面的infer一节中我们实现了用于获取函数返回值的ReturnType
:
type ReturnType<T extends (…args: any) => any> = T extends (…args: any) => infer R ? R : any;
其实把infer换个位置,比如放到返回值处,它就变成了获取参数类型的Parameters
:
type Parameters<T extends (…args: any) => any> = T extends (…args: infer P) => any ? P : never;
如果再大胆一点,把普通函数换成类的构造函数,那么就得到了获取构造函数入参类型的ConstructorParameters
:
type ConstructorParameters<T extends new (…args: any) => any> = T extends new (…args: infer P) => any ? P : never;
加上
new
关键字来使其成为可实例化类型声明
把待infer的类型放到其返回处,想想new一个类会得到什么?实例!所以我们得到了实例类型InstanceType
:
type InstanceType<T extends new (…args: any) => any> = T extends new (…args: any) => infer R ? R : any;
这几个例子看下来,你应该已经get到了那么一丝天机,类型编程的确没有特别高深晦涩的语法,它考验的是你对其中基础部分如索引、映射、条件类型的掌握程度,以及举一反三的能力。下面我们要学习的社区工具类型,本质上还是各种基础类型的组合,只是从常见场景下出发,补充了官方没有覆盖到的部分。
社区工具类型
这一部分的工具类型大多来自于utility-types,其作者同时还有react-redux-typescript-guide 和 typesafe-actions这两个优秀作品。
我们由浅入深,先封装基础的类型别名和对应的类型守卫,不对原理做讲述:
export type Primitive =
| string
| number
| bigint
| boolean
| symbol
| null
| undefined;
export const isPrimitive = (val: unknown): val is Primitive => {
if (val === null || val === undefined) {
return true;
}
const typeDef = typeof val;
const primitiveNonNullishTypes = [
“string”,
“number”,
“bigint”,
“boolean”,
“symbol”,
];
return primitiveNonNullishTypes.indexOf(typeDef) !== -1;
};
export type Nullish = null | undefined;
export type NonUndefined = A extends undefined ? never : A;
// 实际上是TS内置的
type NonNullable = T extends null | undefined ? never : T;
Falsy
和isFalsy
我们已经在上面体现了~
趁着对infer的记忆来热乎,我们再来看一个常用的场景,提取Promise的实际类型:
const foo = (): Promise => {
return new Promise((resolve, reject) => {
resolve(“linbudu”);
});
};
// Promise
type FooReturnType = ReturnType;
// string
type NakedFooReturnType = PromiseType;
如果你已经熟练掌握了infer
的使用,那么实际上是很好写的,只需要用一个infer
参数作为Promise的泛型即可:
export type PromiseType<T extends Promise> = T extends Promise
? U
: never;
使用infer R
来等待类型系统推导出R
的具体类型。
递归的工具类型
前面我们写了个Partial
Readonly
Required
等几个对接口字段进行修饰的工具类型,但实际上都有局限性,如果接口中存在着嵌套呢?
type Partial = {
P in keyof T?: T[P];
};
理一下逻辑:
-
如果不是对象类型,就只是加上
?
修饰符 -
如果是对象类型,那就遍历这个对象内部
-
重复上述流程。
是否是对象类型的判断我们见过很多次了, T extends object
即可,那么如何遍历对象内部?实际上就是递归。
export type DeepPartial = {
P in keyof T?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
utility-types
内部的实现实际比这个复杂,还考虑了数组的情况,这里为了便于理解做了简化,后面的工具类型也同样存在此类简化。
那么DeepReadobly
DeepRequired
也就很简单了:
export type DeepMutable = {
-readonly P in keyof T: T[P] extends object ? DeepMutable<T[P]> : T[P];
};
// 即DeepReadonly
export type DeepImmutable = {
-readonly P in keyof T: T[P] extends object ? DeepMutable<T[P]> : T[P];
};
export type DeepNonNullable = {
};
返回键名的工具类型
在有些场景下我们需要一个工具类型,它返回接口字段键名组成的联合类型,然后用这个联合类型进行进一步操作(比如给Pick或者Omit这种使用),一般键名会符合特定条件,比如:
-
可选/必选/只读/非只读的字段
-
(非)对象/(非)函数/类型的字段
来看个最简单的函数类型字段FunctionTypeKeys
:
export type FunctTypeKeys = {
[K in keyof T]-?: T[K] extends Function ? K : never;
}[keyof T];
{ [K in keyof T]: ... }[keyof T]
这个写法可能有点诡异,拆开来看:
interface IWithFuncKeys {
a: string;
b: number;
c: boolean;
d: () => void;
}
type WTFIsThis = {
[K in keyof T]-?: T[K] extends Function ? K : never;
};
type UseIt1 = WTFIsThis;
很容易推导出UseIt1
实际上就是:
type UseIt1 = {
a: never;
b: never;
c: never;
d: “d”;
}
UseIt
会保留所有字段,满足条件的字段其键值为字面量类型(值为键名)
加上后面一部分:
// “d”
type UseIt2 = UseIt1[keyof UseIt1]
这个过程类似排列组合:never
类型的值不会出现在联合类型中
// string | number
type WithNever = string | never | number;
复制代码
所以{ [K in keyof T]: ... }[keyof T]
这个写法实际上就是为了返回键名(准备的说是键名组成的联合类型)。那么非函数类型字段也很简单了,这里就不做展示了,下面来看可选字段OptionalKeys
与必选字段RequiredKeys
,先来看个小例子:
type WTFAMI1 = {} extends { prop: number } ? “Y” : “N”;
type WTFAMI2 = {} extends { prop?: number } ? “Y” : “N”;
如果能绕过来,很容易就能得出来答案。如果一时没绕过去,也很简单,对于前面一个情况,prop
是必须的,因此空对象{}
并不能继承自{ prop: number }
,而对于可选情况下则可以。因此我们使用这种思路来得到可选/必选的键名。
-
{} extends Pick<T, K>
,如果K
是可选字段,那么就留下(OptionalKeys,如果是RequiredKeys就剔除)。 -
怎么剔除?当然是用
never
了。
export type RequiredKeys = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];
这里是剔除可选字段,那么OptionalKeys就是保留了:
export type OptionalKeys = {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];
只读字段IMmutableKeys
与非只读字段MutableKeys
的思路类似,即先获得:
interface MutableKeys {
readonlyKeys: never;
notReadonlyKeys: “notReadonlyKeys”;
}
然后再获得不为never
的字段名即可。这里还是要表达一下对作者的敬佩,属实巧妙啊,首先定义一个工具类型IfEqual
,比较两个类型是否相同,甚至可以比较修饰前后的情况下,也就是这里只读与非只读的情况。
type Equal<X, Y, A = X, B = never> = (() => T extends X ? 1 : 2) extends <
T
() => T extends Y ? 1 : 2
? A
: B;
-
不要被
<T>() => T extends X ? 1 : 2
干扰,可以理解为就是用于比较的包装,这一层包装能够区分出来只读与非只读属性。 -
实际使用时(非只读),我们为X传入接口,为Y传入去除了只读属性
-readonly
的接口,为A传入字段名,B这里我们需要的就是never,因此可以不填。
实例:
export type MutableKeys = {
P in keyof T-?: Equal<
{ [Q in P]: T[P] },
{ -readonly [Q in P]: T[P] },
P,
never
;
}[keyof T];
几个容易绕弯子的点:
-
泛型Q在这里不会实际使用,只是映射类型的字段占位。
-
X Y同样存在着 分布式条件类型, 来依次比对字段去除readonly前后。
同样的有:
export type IMmutableKeys = {
P in keyof T-?: Equal<
{ [Q in P]: T[P] },
{ -readonly [Q in P]: T[P] },
never,
P
;
}[keyof T];
- 这里不是对
readonly
修饰符操作,而是调换条件类型的判断语句。
基于值类型的Pick与Omit
前面我们实现的Pick与Omit是基于键名的,假设现在我们需要按照值类型来做选取剔除呢?其实很简单,就是T[K] extends ValueType
即可:
export type PickByValueType<T, ValueType> = Pick<
T,
{ [Key in keyof T]-?: T[Key] extends ValueType ? Key : never }[keyof T]
;
export type OmitByValueType<T, ValueType> = Pick<
T,
{ [Key in keyof T]-?: T[Key] extends ValueType ? never : Key }[keyof T]
;
条件类型承担了太多…
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
结尾
学习html5、css、javascript这些基础知识,学习的渠道很多,就不多说了,例如,一些其他的优秀博客。但是本人觉得看书也很必要,可以节省很多时间,常见的javascript的书,例如:javascript的高级程序设计,是每位前端工程师必不可少的一本书,边看边用,了解js的一些基本知识,基本上很全面了,如果有时间可以读一些,js性能相关的书籍,以及设计者模式,在实践中都会用的到。
{ -readonly [Q in P]: T[P] },
never,
P
;
}[keyof T];
- 这里不是对
readonly
修饰符操作,而是调换条件类型的判断语句。
基于值类型的Pick与Omit
前面我们实现的Pick与Omit是基于键名的,假设现在我们需要按照值类型来做选取剔除呢?其实很简单,就是T[K] extends ValueType
即可:
export type PickByValueType<T, ValueType> = Pick<
T,
{ [Key in keyof T]-?: T[Key] extends ValueType ? Key : never }[keyof T]
;
export type OmitByValueType<T, ValueType> = Pick<
T,
{ [Key in keyof T]-?: T[Key] extends ValueType ? never : Key }[keyof T]
;
条件类型承担了太多…
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-6OGNkt77-1713684641399)]
[外链图片转存中…(img-0HjL1zcT-1713684641399)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
[外链图片转存中…(img-RzIvvQRV-1713684641399)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
[外链图片转存中…(img-dZeT2jQM-1713684641400)]
结尾
学习html5、css、javascript这些基础知识,学习的渠道很多,就不多说了,例如,一些其他的优秀博客。但是本人觉得看书也很必要,可以节省很多时间,常见的javascript的书,例如:javascript的高级程序设计,是每位前端工程师必不可少的一本书,边看边用,了解js的一些基本知识,基本上很全面了,如果有时间可以读一些,js性能相关的书籍,以及设计者模式,在实践中都会用的到。