基础知识
类型可以简单看成一个值的集合,在做类型运算的时,可分为两类:
- 包含一个元素的类型(值):
0
,"foo"
; - 包含多个(无限个)元素的类型:
number
,string
;
还有两个特殊的类型:any
全集,never
空集;
交叉类型,可把现有的类型合并为新的类型,如type A = B & C;
,A 可同时获得 B 和 C 的所有属性;当存在相同属性而类型不一致时,会得到never
类型;
当同种属性其一被readonly
修饰时,由于是交叉会取合并类型,即readonly
会被“覆盖”;相关说明
按照我的理解,readonly
是在编译类型检查时把setter
屏蔽了,类似于const
,简单来看,可看作被修饰的属性没有setter
。那么联合类型则是取相同(getter
)的,交叉类型则是全都取(getter & setter
)。
1 | type P1 = Pick<{ readonly p: any } | { p: any }, "p"> // { readonly p: any; } |
A extends B ? C : D
的意义是判断 A
集合是否为 B
集合的子集,如果是那么返回 C
否则返回 D
;以此可以引申出对值和类型的运算:
- 判断值是否相等:
===
; - 判断值是否属于某个类型:
typeof
;
type A<B, C> = D
,自定义类型,可以当作是一个函数,等号左边泛型可进行输入作为函数参数,等号右边是输出,也就是会返回的类型;
type ParamType<T> = T extends (...args: infer P) => any ? P : T;
,意为如果 T
能赋值给 (...args: infer P) => any
,则结果是 (...args: infer P) => any
类型中的参数 P
,否则返回为 T
,infer P
表示待推断的函数参数;infer
可以类比纯函数式语言中的声明局部变量;
貌似infer
有点像 ES6 中的解构,实际上,这种称为模式匹配:也就是拿到匹配后确切的值后再拆解;
工具泛型
Partial
1 | /** |
将传入的属性变为可选类型,代码的核心在于 keyof
和 in
,keyof
用于取得一个接口所有 key
值(联合类型),而in
是用于迭代联合类型中的所有元素,用于在映射类型(Mapped Types)中,上述代码[P in keyof T]?: T[P]
即为一个映射类型;
而且in
关键字还能用于在类型守卫上,例如:
1 | interface A { |
回到正题,在映射类型Partial
通过传入泛型<T>,获取的 T 的 key 的联合类型,然后迭代成一个新的类型,新的类型的 key 还是对应着T类型中的 value,不过都变为了可选属性。
Required
1 | /** |
将 T 中的类型都变成必选类型,其中-
符号表示移除某个映射修饰符(Mapping Modifiers),如?
可选,readonly
只读,如上的Partial
则为未指定情况,假定为+
;
Readonly
1 | /** |
将所有属性设置为只读,若设置为-readonly
,则为移除属性的 Readonly 修饰符;
Pick
1 | /** |
从 T 中挑选出一组 key 存在于联合类型 K 中的属性,简而言之,就是从 T 中选择几个指定的属性;
这里指定传入类型时使用了extends
作为判断的标准,必须满足keyof T
,即 K 必须是 T 的 key 才行;
内部又是一个映射类型,将欲选出的属性(key 和 value)作为新类型返回;
Record
1 | /** |
构造一个属性类型为 K,值类型为 T 的类型;注意这有个 keyof any
很耐人寻味,首先我们能看到 K 在满足extends keyof any
时的类型为string | number | symbol
,这正是 ts 中允许被作为对象的索引的类型,这样我们就能明白了:对 any
取 keyof
操作,可以得到所有的索引的类型,毕竟是 any ,这样也就限制住了 K 的类型。
另一个需要注意的地方,若多个 Record
泛型进行交叉时,Record
若有相同的 key,不同的 value,则会 value 会被推导为 never
:
1 | type testRecord = Record<string, string> & Record<string,number> |
也可以写一个泛型工具对交叉类型进行合并,可以查看的更直观:
1 | export type MergeInsertions<T> = |
Exclude
1 | /** |
与 Pick 相反,Exclude 用于在 T 中排除可分配给 U 的子类型;这里有一个很重要的概念,分发条件类型(Distributive Conditional Types),上述的 Exclude 类型若 T 被指定为联合类型,则构成了分配条件类型,官网的一个例子如下:
1 | type ToArray<Type> = Type extends any ? Type[] : never; |
总结来说就是extends
会将联合类型拆开,分别传入类型定义,如此的设定可以让我们过滤联合类型的特定成员。通常,联合类型是默认分配的,但是能通过[]
将extends
两边的泛型参数包裹起来,如:
1 | type P<T> = [T] extends [any] ? string : number; |
这里有一个特殊情况,就是 never
,如下:
1 | type P<T> = T extends any ? string : number; |
Test1
的结果是因为string
是任何类型的子类型,返回string
,;
Test2
的结果是never
,在此情况下,never
被认为是空的联合类型,因为没有联合项可以分配,即P<T>
的表达式其实根本就没有执行,也就类似于没有返回值的函数,是 never 类型;
ThisType
ThisType 的作用是可以在对象字面量中指定 this 的类型。ThisType 不返回转换后的类型,而是通过 ThisType 的泛型参数指定 this 的类型;
注意:如果你想使用这个工具类型,那么需要在 tsconfig.json 中开启
noImplicitThis
配置;
1 | type ObjectDescriptor<D, M> = { |
methods
属性的 this
类型为 D & M
,在上下文中指代 { x: number, y: number } & { moveBy(dx: number, dy: number): void }
。这是官方文档中的例子,看起来其实并不直观;
简单来说,对某一个对象进行& ThisType<SomeType>
那么就能指定该对象中 this 的类型为SomeType
;
ThisType 工具类型实际上只是提供了一个空的泛型接口,仅可以在对象字面量上下文中被 TypeScript 识别,也就是说该类型的作用相当于任意空接口。
1 | interface ThisType<T> {} |
类型体操
hard-tuple-to-enum-object
传入一个元组,将元组转换为枚举对象,如果第二个参数为 true,那么值应该是数字字面量
1 | type Format<T extends readonly string[], P extends any[] = []> |
首先来看Format
泛型,传入一个字符串数组和 any 数组,内部是一个递归,作用是提取到传入的元组的值及其序列。
首先会使用infer
推导提取第一个元素R
,以及剩余元素S
,然后根据S
是否为空判断是否终止递归;终止返回空数组,否则返回一个数组,内部也是数组结构,内部第一个数组[R, P["length"]]
包括第一个元素和传入的数组P
的长度,因为默认是传入空数组的,所以第一个元素的该值是 0;之后是递归的操作,会将剩余元素S
以及[...P, any]
传入Format
中,[...P, any]
这个操作可谓一绝,直接将数组元素加一,然后之后的元素进行P["length"]
推导时即完成了枚举类型的递增操作。
有了上述的基础操作,那么Enum
操作就简单了。按照题目要求,传入数组和布尔值。Format<T>[number]
拿到Format
匹配结果的数组元素的联合类型,as
操作符对in
遍历联合类型的结果做一个转换,即取到[0]
的值并利用内置的Capitalize
工具泛型将字符串的首字母大写。最后则是判断F
,返回K[1]
或K[0]
。
当然也没必要这么复杂地将元组格式化成新的数据结构,简单地拿到每一个值的就很够的了:
1 | type FindIndex<T extends readonly any[], K, A extends any[] = []> |