Type-Challenges
介绍
在学习完TypeScript
一些基础知识后,我们已经可以熟练使用一些基本类型定义了,但对于TypeScript
的高级用法却依旧无法入门,为了更有趣的学习TypeScript
高级用法,我们选择Type-Challenges类型挑战来作为我们学习的目标。
在Type-Challenges
中,可以从简单(easy
)、中等(medium
)、困难(hard
)以及地狱(extreme
)难度,循序渐进的学习TypeScript
高级技巧。
如果你需要选择其它的方向来深入学习TypeScript
高级技巧,这里也有一些推荐的开源项目:
- 官方内置:在
lib.es5.d.ts
文件中,TypeScript
官方默认内置了一些辅助工具函数,例如:Partial
、Required
、Pick
以及Record
等等。 - 其它开源库:utility-types、ts-toolbelt、SimplyTyped
在之后的挑战中,我们会尽力对每道题进行必要的讲解,力争在进行Type-Challenges
类型挑战时弄清楚所有涉及到的知识点。
核心知识点
加号和减号
提示
加号和减号的用法类似。
在一些内置工具中,可能会出现+
或者-
这些符号,例如:
type Required<T> = {
[P in keyof T]-?: T[P]
}
type Person = {
name: string;
age?: number;
}
// 结果:{ name: string; age: number; }
type result = Required<Person>
观察以上结果可以得出结论:-?
是去掉类型中属性后面的?
,整个Required
的实际效果是去掉T
类型中所有属性键后面的?
,让所有属性变成必填的。
keyof 和 in
keyof
和in
经常会连在一起使用,当它们连在一起使用时,通常表示一个迭代的过程。
keyof
在TS
中,keyof T
这段代码表示获取T
类型中所有属性键,这些属性键组合成一个联合类型,例如:
type Person = {
name: string;
age: number;
}
// 结果:'name' | 'age'
type result = keyof Person
TS
中的keyof T
,它有点类似JavaScript
中的Object.keys()
,它们的共同点都是获取属性键的集合,只不过keyof T
得到的结果是一个联合类型,而Object.keys()
得到的是一个数组。
in
in
操作符的右侧通常跟一个联合类型,可以使用in
来迭代这个联合类型,如下:
// 仅演示使用, K为每次迭代的项
K in 'name' | 'age' | 'sex'
K = 'name' // 第一次迭代结果
K = 'age' // 第二次迭代结果
K = 'sex' // 第三次迭代结果
根据keyof
和in
的特点,我们可以撰写一些辅助工具,这里以Readonly
为例。
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
type Person = {
name: string;
age: number;
}
// 结果:{ readony name: string; readonly age: number; }
type result = Readonly<Person>
代码详解:
[P in keyof T]
:这段代码表示遍历T
中的每一个属性键,每次遍历时属性键取名为P
,这和JavaScript
中的for in
非常类似:
// ts中的迭代
P in keyof T
// js中的迭代
for (let key in obj)
typeof
TS
中的typeof
,可以用来获取一个JavaScript
变量的类型,经常用于获取一个普通对象或者一个函数的类型,如下:
const add = (a: number, b: number): number => {
return a + b
}
const obj = {
name: 'AAA',
age: 23
}
// 结果:(a: number, b:number) => number
type t1 = typeof add
// 结果:{ name: string; age: number; }
type t2 = typeof obj
never
never
类型表示永远不会有值的一种类型。
例如,如果一个函数抛出一个错误,那么这个函数就可以用never
或者void
来表示其返回值,如下:
// never更适合用来表示永远没有返回值的函数
function handlerError(message: string): never {
throw new Error(message)
}
// void适合用来表示返回值为空的函数
function handlerError(message: string): void {
throw new Error(message)
}
关于never
的另外一个知识点是:如果一个联合类型中存在never
,那么实际的联合类型并不会包含never
,如下:
// 定义
type test = 'name' | 'age' | never
// 实际
type test = 'name' | 'age'
extends
extends
关键词,一般有两种用法:类型约束和条件类型。
类型约束
类型约束经常和泛型一起使用:
// 类型约束
U extends keyof T
keyof T
是一个整体,它表示一个联合类型。U extends Union
这一整段表示U
的类型被收缩在一个联合类型的范围内。例如: U extends 'name' | 'age'
,则表示U
只能为name
或者age
二者其中之一。
条件类型
常见的条件类型表现形式如下:
T extends U ? 'Y' : 'N'
我们发现条件类型有点像JavaScript
中的三元表达式,事实上它们的工作原理是类似的,例如:
type result1 = true extends boolean ? true : false // true
type result2 = 'name' extends 'name' | 'age' ? true : false // true
type result3 = [1, 2, 3] extends { length: number; } ? true : false // true
type result4 = [1, 2, 3] extends Array<number> ? true : false // true
在条件类型中,有一个特别需要注意的东西就是:分布式条件类型,如下:
// 内置工具:交集
type Extract<T, U> = T extends U ? T : never;
type type1 = 'name'|'age'
type type2 = 'name'|'address'|'sex'
// 交集结果:'name'
type result = Extract<type1, type2>
// 推理步骤
'name'|'age' extends 'name'|'address'|'sex' ? T : never
step1: ('name' extends 'name'|'address'|'sex' ? 'name' : never) => 'name'
step2: ('age' extends 'name'|'address'|'sex' ? 'age' : never) => never
result: 'name' | never => 'name'
代码详解:
T extends U ? T : never
:因为T
是一个联合类型,所以这里适用于分布式条件类型的概念。根据其概念,在实际的过程中会把T
类型中的每一个子类型进行迭代,如下:
// 第一次迭代:
'name' extends 'name'|'address'|'sex' ? 'name' : never
// 第二次迭代:
'age' extends 'name'|'address'|'sex' ? 'age' : never
- 在迭代完成之后,会把每次迭代的结果组合成一个新的联合类型(根据
never
类型的特点,最后的结果会剔除掉never
),如下:
type result = 'name' | never => 'name'
infer
infer
关键词的作用是延时推导,它会在类型未推导时进行占位,等到真正推导成功后,它能准确的返回正确的类型。
为了更好的理解infer
关键词的用法,我们使用ReturnType
这个例子来说明,ReturnType
是一个用来获取函数返回类型的工具。
type ReturnType<T> = T extends (...args: any) => infer R ? R : never
const add = (a: number, b: number): number => {
return a + b
}
// 结果: number
type result = ReturnType<typeof add>
代码详解:
T extends (...args: any) => infer R
:如果不看infer R
,这段代码实际表示:T
是不是一个函数类型。(...args: any) => infer R
:这段代码实际表示一个函数类型,其中把它的参数使用args
来表示,把它的返回类型用R
来进行占位。 如果T
满足是一个函数类型,那么我们返回其函数的返回类型,也就是R
;如果不是一个函数类型,就返回never
。
TS
中的infer
占位更像JavaScript
中的模板字符串:
// 函数的返回类型使用R占位表示
(...args: any) => info R
// 模板字符串中的值,使用变量name占位表示
const str = `hello, ${name}`
& 符号
在TS
中有两种类型值得我们重点关注:联合类型和交叉类型。
联合类型一般适用于基本类型的合并,它使用|
符号进行连接,如下:
type result = 'name' | 1 | true | null
而交叉类型则适用于对象或者函数的合并,它使用&
符号进行连接,如下:
type result = T & U
T & U
表示一个新的类型,其中这个类型包含T
和U
中所有的键,这和JavaScript
中的Object.assign()
函数的作用非常类似。
根据交叉类型的概念,我们可以封装一个合并对象的merge
函数,如下:
// ts v4.8.4以上版本
function merge<T, U, K extends T & U>(to: T, from: U): K {
for (let key in from) {
;(to as unknown as K)[key] = from[key] as any
}
return to as unknown as K
}
// ts v4.8.4以下版本
function merge<T, U, K extends T & U>(to: T, from: U): K {
for (let key in from) {
;(to as K)[key] = from[key] as any
}
return to as K
}
const obj1 = { name: 'AAA' }
const obj2 = { age: 23 }
// js结果:{ name:'AAA'; age: 23; }
// ts结果:{ name: string; age: number; }
const result = merge(obj1, obj2)
初级
内置Pick(选取)
用法
Pick
表示从一个类型中选取指定的几个字段组合成一个新的类型,用法如下:
type Person = {
name: string;
age: number;
address: string;
sex: number;
}
// 结果: { name: string; address: string; }
type PickResult = Pick<Person, 'name' | 'address'>
实现方式
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
代码详解:
K extends keyof T
:表示K
只能是keyof T
的子类型,如果我们在使用Pick
的时候传递了不存在于T
的字段,会报错:
// 报错:phone无法分配给keyof T
type result = MyPick<Person, 'name' | 'phone'>
内置Readonly(只读)
用法
Readonly
是用来让所有属性变为只读,其用法为:
type Person = {
readonly name: string;
age: number;
}
// 结果:{ readonly name: string; readonly age: number; }
type result = MyReadonly<Person>
实现方式
type MyReadonly<T> = {
readonly [P in keyof T]: T[P]
}
TupleToObject(元组转对象)
用法
TupleToObject<T>
是用来把一个元组转换成一个key/value
相同的对象,例如:
// 类型:readonly ['msg', 'name']
const tuple = ['msg', 'name'] as const
// 结果:{ msg: 'msg'; name: 'name'; }
type result = TupleToObject<typeof tuple>
实现方式
type TupleToObject<T extends readonly any[]> = {
[P in T[number]]: P
}
代码详解:
as const
:常用来进行常量断言,在此处表示将['msg','name']
推导常量元组,表示其不能新增、删除、修改元素,可以使用as readonly
来辅助理解。T[number]
:表示返回数组中所有数字型索引的元素,形成一个联合类型,例如:'msg'|'name'
。
First(数组第一个元素)
用法
First<T>
用来返回数组的第一个元素,用法如下:
// 结果:3
type result1 = First<[3, 2, 1]>
// 结果:never
type result2 = First<[]>
实现方式
// 索引实现方式
type First<T extends any[]> = T extends [] ? never : T[0]
// 占位实现方式
type First<T extends any[]> = T extends [infer R, ...infer L] ? R : never
代码详解:
T extends []
:用来判断T
是否是一个空数组。T[0]
:根据下标取数组第一个元素。infer R
: 表示数组第一个元素的占位。...infer L
: 表示数组剩余元素的占位。
Length(元组的长度)
用法
Length<T>
用来获取一个数组(包括类数组)的长度,用法如下:
// 结果:3
type result1 = Length<[1, 2, 3]>
// 结果:10
type result2 = Length<{ 5: '5', length: 10 }>
实现方式
type Length<T extends any> = T extends { length: number; } ? T['length'] : never
代码详解:
T extends { length: number; }
:判断T
是否是{ length: number; }
的子类型,如果是则代表T
为数组或者类数组。T['length']
:取T
对象的length
属性的值(注意,在TypeScript
中不能使用T.length
来取值,而应该使用T['length']
)。
内置Exclude(排除)
用法
Exclude
是排除的意思,它从T
类型中排除属于U
类型的子集,可以理解成取T
对于U
的差集,用法如下:
// 结果:'name'|'age'
type ExcludeResult = Exclude<'name'|'age'|'sex', 'sex'|'address'>
实现方式
type MyExclude<T, U> = T extends U ? never : T
T extends U
:这段代码会从T
的子类型开始分发,例如:
T extends U
=> 'name'|'age'|'sex' extends 'sex'|'address'
=> (
'name' extends 'sex'|'address' ? never : 'name' |
'age' extends 'sex'|'address' ? never : 'age' |
'sex' extends 'sex'|'address' ? never : 'sex'
)
=> 'name'|'age'
PromiseType(promise包裹类型)
用法
PromiseType
是用来获取Promise
包裹类型的,例如:
function getInfo (): Promise<string|number> {
return Promise.resolve(1)
}
// 结果:() => Promise<string|number>
type funcType = typeof getInfo
// 结果:Promise<string|number>
type returnResult = ReturnType<funcType>
// 结果:string|number
type PromiseResult = PromiseType<returnResult>
实现方式
type PromiseType<T> =
T extends Promise<infer R>
? R extends Promise<any>
? PromiseType<R>
: R
: never
代码详解:
T extends Promise<infer R>
:判断T
是否是Promise<infer R>
的子类型,也就是说T
必须满足Promise<any>
的形式。
If(判断)
用法
If<C, T, F>
用来表示根据C
的值来返回T
或者F
,如果C
为true
,则返回T
;如果C
为false
,则返回F
,例如:
// 结果:'a'
type result1 = If<true, 'a', 'b'>
// 结果:'b'
type result2 = If<false, 'a', 'b'>
根据上案例,我们可以直观的发现If<C, T, F>
的作用有点类似JavaScript
中的三元表达式:C ? T : F
。
实现方式
type If<C extends boolean, T, F> = C extends true ? T : F
代码详解:
C extends boolean
:表示C
为boolean
类型的子类型,既C
只能为true
或者false
,传递其它值报错。C extends true
:如果用JavaScript
来表示的话,相当于C===true
.
Concat(数组concat方法)
用法
Concat<T, U>
用来将两个数组合并起来,类似实现数组的concat
方法,使用方式如下:
// 结果:[1, 2, 3, 4]
type result = Concat<[1, 2], [3, 4]>
实现方式
type Concat<T extends any[], U extends any[]> = [...T, ...U]
代码详解:
T extends any[]
:用来限制T
是一个数组,如果传递非数组会报错,U
也是一样的道理。[...T, ...U]
:可以理解成JavaScript
的扩展运算符...
。
Includes(数组includes方法)
用法
Includes<T, U>
用来判断U
是否在数组T
中,类似实现数组的includes
方法,用法如下:
// 结果:true
type result1 = Includes<[1, 2, 3], 1>
// 结果:false
type result2 = Includes<[1, 2, 3], '1'>
实现方式
type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false
// 简单版
type MyIncludes<T extends readonly any[], U> = U extends T[number] ? true : false
// 完善版
type MyIncludes<T extends readonly any[], U> =
T extends [infer R, ...infer L]
? Equal<R, U> extends true
? true
: MyIncludes<L, U>
: false
代码详解:
T[number]
:它返回数组中所有数字类型键对应的值,将这些值构造成一个联合类型,例如:1 | 2 | 3
。U extends T[number]
:判断U
是否是某个联合类型的子类型,例如:1 extends 1 | 2 | 3
。Equal
:是用来判断两个值是否相等的辅助方法。
Push(数组push方法)
用法
// 结果:[1, 2, 3, 4]
type result = Push<[1, 2, 3], 4>
实现方式
// Push实现
type Push<T extends any[], K> = [...T, K]
Unshift(数组unshift方法)
与pop
和push
方法相似的另外一对方法叫shift
和unshift
,它们的实现思路是一样的。
用法
// 结果:[0, 1, 2, 3]
type result = Unshift<[1, 2, 3], 0>
实现方式
// Unshift实现
type Unshift<T extends any[], K> = [K, ...T]
内置Parameters(函数的参数类型)
用法
Parameters
是用来获取一个函数的参数类型的,其中获取的结果是一个元组,用法如下:
const add = (a: number, b: string): void => {}
// [number, string]
type result = MyParameters<typeof add>
实现方式
type MyParameters<T extends (...args: any[]) => any> = T extends (...args: infer R) => any ? R : never
内置Partial(可填)和内置Required(必填)
提示
此题不属于type-challenges类型挑战题
用法
Partial
和Required
一个是让所有属性可填、另外一个是让所有属性必填,用法如下:
type Person = {
name: string;
age?: number;
}
// 结果: { name?: string; age?: number; }
type PartialResult = MyPartial<Person>
// 结果: { name: string; age: number; }
type RequiredResult = MyRequired<Person>
实现方式
type MyPartial<T> = {
[P in keyof T]?: T[P]
}
type MyRequired<T> = {
[P in keyof T]-?: T[P]
}
内置Record(构造)
提示
此题不属于type-challenges类型挑战题
用法
Record<K, T>
用来将K
的每一个键(k
)指定为T
类型,这样由多个k/T
组合成了一个新的类型,用法如下:
type keys = 'Cat'|'Dot'
type Animal = {
name: string;
age: number;
}
type Expected = {
Cat: {
name: string;
age: number;
};
Dog: {
name: string;
age: number;
}
}
// 结果:Expected
type RecordResult = Record<keys, Animal>
实现方式
type MyRecord<K extends keyof any, T> = {
[P in K]: T
}
代码详解:
K extends keyof any
:此代码表示K
是keyof any
任意类型其所有键的子类型,例如:
// K为 'Dog'|'cat'
type UnionKeys = 'Dog' | 'Cat'
// K为'name'|'age'
type Person = {
name: string;
age: number;
}
type TypeKeys = keyof Person
内置Extract(交集)
提示
此题不属于type-challenges类型挑战题
用法
Extract<T, U>
用来取联合类型T
和U
的交集,用法如下:
type Person = {
name: string;
age: number;
address: string;
}
// 结果:'age'|'address'
type ExtractResult = Extract<keyof Person, 'age'|'address'|'sex'>
实现方式
type MyExtract<T, U> = T extends U ? T : never
代码详解:
T extends U
:此代码会自动将T
的子类型进行分发,例如:
T extends U
=> 'name'|'age'|'address' extends 'age'|'address'|'sex' ? T : never
=> (
'name' extends 'age'|'address'|'sex' ? 'name' : never |
'age' extends 'age'|'address'|'sex' ? 'age' : never |
'address' extends 'age'|'address'|'address' ? 'age' : never
)
=> 'age'|'address'
中级
内置ReturnType(函数返回类型)
用法
ReturnType<T>
是用来获取一个函数的返回类型的,例如:
function getRandom (): number {
return Math.random()
}
// 结果:number
type result = ReturnType<typeof getRandom>
实现方式
type ReturnType<T> = T extends (...args: any) => infer R ? R : never
代码详解:
T extends (...args: any) => infer R
:判断T
类型是否是一个函数的子类型,既T
是不是一个函数。infer R
:表示待推导的函数返回类型为R
,后续可以在表达式中使用R
来代替真正的返回类型。
内置Omit(移除)
用法
Omit
是移除的意思,它用来在T
类型中移除指定的字段,用法如下:
type Person = {
name?: string;
age: number;
address: string;
}
// 结果:{ name?: string; age: number; }
type OmitResult = Omit<Person, 'address'>
实现方式
Omit
可以借助在上面已经实现过的Pick
和Exclude
配合来实现,如下:
// Omit实现
type MyOmit<T, K> = MyPick<T, MyExclude<keyof T, K>>
代码详解:
- 使用
MyExclude<keyof T, K>
,可以从T
中移除指定的字段,移除后得到一个新的联合类型:'name'|'age'
- 使用
MyPick<T, 'name'|'age'>
,可以从T
中选取这两个字段,组合成一个新的类型。
Readonly(按需Readonly)
用法
不同于初级实现中的Readonly
,在中级实现的Readonly
中,如果我们传递了指定的字段,那么Readonly
会表现为按需实现readonly
,用法如下。
interface Todo {
title: string;
desc?: string;
completed: boolean;
}
interface Expected1 {
readonly title: string;
readonly desc?: string;
readonly completed: boolean;
}
interface Expected2 {
title: string;
readonly desc?: string;
readonly completed: boolean;
}
// 结果:Expected1
type ReadonlyResult1 = Readonly<Todo>
// 结果:Expected2
type ReadonlyResult2 = Readonly<Todo, 'desc'|'completed'>
// 测试:
const obj: ReadonlyResult2 = {
title: 'AAA',
desc: '23',
completed: true
}
obj.title = 'aaa'
obj.desc = '32' // error
obj.completed = false // error
实现方式
// ts v4.4+版本可直接用
type Readonly<T, K extends keyof T = keyof T> = T & {
readonly [P in K]: T[P]
}
// ts v4.5+版本必须用
type Readonly<T, K extends keyof T = keyof T> = Omit<T, K> & {
readonly [P in K]: T[P]
}
代码详解:
K extends keyof T = keyof T
:如要传递了K
,那么只能是T
中已经存在的属性,不存在则报错;如果不传递,则默认值为keyof T
,意味着全部属性都添加readonly
。T & U
:在本例中表示将T
和U
中的字段结合起来,如果没有&
会丢失一些属性,例如title
。
DeepReadonly(深度Readonly)
用法
DeepReadonly
用来将一个嵌套对象类型中所有字段全部添加readonly
关键词,例如:
// 类型:
type X = {
b: string
c: {
d: boolean
e: undefined,
f: null
}
}
// 结果:
type Y = {
readonly b: string
readonly c: {
readonly d: boolean
readonly e: undefined,
readonly f: null
}
}
实现方式
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends { [key: string]: any } ? DeepReadonly<T[P]> : T[P]
}
代码详解:
T[P] extends { [key: string]: any }
:这段表示T[P]
是否是一个包含索引签名的字段,如果包含我们认为它是一个嵌套对象,就可以递归调用DeepReadonly
。
TupleToUnion(元组转联合类型)
用法
TupleToUnion
是用来将一个元组转换成联合类型的,其用法如下:
// 结果:'1' | '2' | '3'
type result = TupleToUnion<['1', '2', '3']>
实现方式
// way1: T[number]
type TupleToUnion<T extends readonly any[]> = T[number]
// way2: 递归
type TupleToUnion<T extends readonly any[]> =
T extends readonly [infer R, ...infer args]
? R | TupleToUnion<args>
: never
代码详解:
T[number]
:它会自动迭代元组的数字型索引,然后将所有元素组合成一个联合类型。R | TupleToUnion<args>
:R
表示每一次迭代中的第一个元素,它的迭代过程可以用下面伪代码表示:
// 第一次迭代
const R = '1'
const args = ['2', '3']
const result = '1' | TupleToUnion<args>
// 第二次迭代
const R = '2'
const args = ['3']
const result = '1' | '2' | TupleToUnion<args>
// 第三次迭代
const R = '3'
const args = ['']
const result = '1' | '2' | '3'
Chainable(可串联构造器)
用法
Chainable
是用来让一个对象可以进行链式调用的,用法如下:
type Expected = {
foo: number
bar: {
value: string
}
name: string
}
declare const obj: Chainable<{}>
// 结果:Expected
const result = obj
.options('foo', 123)
.options('bar', { value: 'Hello' })
.options('name', 'TypeScript')
.get()
实现方式
type Chainable<T> = {
options<K extends string, V>(key: K, value: V): Chainable<T & {[k in K]: V}>
get(): T
}
代码详解:
{[k in K]: V}
:每次调用options
时,把key/value
构造成一个对象,例如:{ foo: 123 }
。T & U
:此处使用到&
关键词,用来合并T
和U
两个对象中的所有key
。Chainable<>
:递归调用Chainable
,赋予新对象以链式调用的能力。
Last(数组最后一个元素)
用法
Last
是用来获取数组中最后一个元素的,它和我们之前已经实现的First
思路很相似。
// 结果:3
type result = Last<[1, 2, 3]>
实现方式
Last
的实现方式很巧妙,因为它既可以在索引上做文章来实现,也可以用占位的思想来实现。
// way1:索引思想
type Last<T extends any[]> = [any, ...T][T['length']]
// way2: 后占位思想
type Last<T extends any[]> = T extends [...infer R, infer L] ? L : never
代码详解:
[any, ...T]
:此代码表示我们构建了一个新数组,并添加了一个新元素到第一个位置,然后把原数组T
中的元素依次扩展到新数组中,可以用以下伪代码表示:
// 原数组
const T = [1, 2, 3]
// 新数组
const arr = [any, 1, 2, 3]
// 结果: 3
const result = arr[T['length']]
T['length']
:这里我们获取到的是原始T
数组的长度,例如[1, 2, 3]
,长度值为3
。而在新数组中,索引为3
的位置正好是最后一个元素的索引,通过这种方式就能达到我们的目的。T extends [...infer R, infer L]
:这段代码表示,我们将原数组中最后一个元素使用L
进行占位,而其它元素我们用一个R
数组表示。这样,如果数组满足这种格式,就能正确返回最后一个元素的值。
Pop(数组Pop方法)
继续沿用以上处理索引思想或占位的思想,我们能快速实现数组pop
方法。
用法
// 结果1:[1, 2]
type result1 = Pop<[1, 2, 3]>
// 结果2:[]
type result2 = Pop<[]>
实现方式
// Pop实现
type Pop<T extends any[]> =
T extends []
? []
: T extends [...infer Rest, infer L]
? Rest
: never
PromiseAll返回类型
用法
PromiseAll
是用来取Promise.all()
函数所有返回的类型,其用法如下
const result1 = PromiseAll([1, 2, 3] as const)
const result2 = PromiseAll([1, 2, Promise.resolve(3)] as const)
const result3 = PromiseAll([1, 2, Promise.resolve(3)])
const result4 = PromiseAll<Array<number | Promise<number>>>([1, 2, 3])
// 结果1: Promise<[1, 2, 3]>
type t1 = typeof result1
// 结果2: Promise<[1, 2, number]>
type t2 = typeof result2
// 结果3: Promise<[number, number, number]>
type t3 = typeof result3
// 结果4: Promise<number[]>
type t4 = typeof result4
实现方式
与之前的例子不同,PromiseAll
我们声明的是一个function
而不是type
。
// Awaited为内置类型
type PromiseAllType<T> = Promise<{
[P in keyof T]: Awaited<T[P]>
}>
declare function PromiseAll<T extends any[]>(values: readonly [...T]): PromiseAllType<T>
代码详解:
- 因为
Promise.all()
函数接受的是一个数组,因此泛型T
限制为一个any[]
类型的数组。 PromiseAllType
的实现思路有点像之前的PromiseType
,只不过这里多了一层Promise
的包裹,因为Promise.all()
的返回类型也是一个Promise
。
LookUp(查找)
用法
LookUp
是用来根据类型值查type
找类型的,其用法如下:
interface Cat {
type: 'cat'
color: 'black' | 'orange' | 'gray'
}
interface Dog {
type: 'dog'
color: 'white'
name: 'wang'
}
// 结果:Dog
type result = LookUp<Cat | Dog, 'dog'>
实现方式
type LookUp<
U extends { type: string; },
T extends string
> = U extends { type: T } ? U : never
代码详解:
U extends { type: string; }
:这段代码限制U
的类型必须是具有属性为type
的对象。U extends { type: T }
:如果把T
的值实际带入,为U extends { type: 'dog' }
,表示判断U
中的type
值是不是dog
,是则返回U
。
Trim、TrimLeft以及TrimRight
TrimLeft:
TrimRight: Trim: TrimRight: Trim:用法
Trim
、TrimLeft
以及TrimRight
这几个工具比较好理解,它们都是用来移除字符串中的空白符的。
实现方式
type Space = ' ' | '\n' | '\t'
type TrimLeft<S extends string> = S extends `${Space}${infer R}` ? TrimLeft<R> : S
type Trim<S extends string> = S extends (`${Space}${infer R}` | `${infer R}${Space}`) ? Trim<R> : S
type TrimRight<S extends string> = S extends `${infer R}${Space}` ? TrimRight<R> : S
代码详解:
TrimLeft
和TrimRight
的实现思路是相同的,区别在于空白符的占位出现在左侧还是右侧。Trim
的实现就是把TrimLeft
和TrimRight
所做的事情结合起来。
Capitalize(首字母大写)和UnCapitalize(首字母小写)
用法
Capitalize
是用来将一个字符串的首字母变成大写的,而UnCapitalize
所做的事情跟它相反,其用法如下:
实现方式
type Capitalize<S extends string> = S extends `${infer char}${infer L}` ? `${Uppercase<char>}${L}` : S
type UnCapitalize<S extends string> = S extends `${infer char}${infer L}` ? `${Lowercase<char>}${L}` : S
代码详解:
- 无论是
Capitalize
还是UnCapitalize
,它们都依赖内置的工具函数Uppercase
或者Lowercase
。对于Capitalize
而言,我们只需要把首字母隔离出来,然后调用Uppercase
即可。对于UnCapitalize
而言,我们把首字母调用Lowercase
即可。
Replace
Replace
是用来将字符串中第一次出现的某段内容,使用指定的字符串进行替换,而ReplaceAll
是全部替换,其用法如下:
// 结果1:'foofoobar'
type t1 = Replace<'foobarbar', 'bar', 'foo'>
// 结果2: foobarbar
type t2 = Replace<'foobarbar', '', 'foo'>
实现方式
type Replace<
S extends string,
from extends string,
to extends string
> = S extends `${infer L}${from}${infer R}`
? from extends ''
? S
: `${L}${to}${R}`
: S
ReplaceAll
ReplaceAll
是用来将字符串中指定字符全部替换的,其用法如下:
// 结果:'foofoofoo'
type t = ReplaceAll<'foobarbar', 'bar', 'foo'>
实现方式
type ReplaceAll<
S extends string,
from extends string,
to extends string
> = S extends `${infer L}${from}${infer R}`
? from extends ''
? S
: `${ReplaceAll<L, from, to>}${to}${ReplaceAll<R, from, to>}`
: S
AppendArgument(追加参数)
用法
AppendArgument
是用来向一个函数追加一个参数的,其用法如下:
实现方式
type AppendArgument<Fn, A> = Fn extends (...args: infer R) => infer T ? (...args: [...R, A]) => T : never
代码详解:
- 我们首先利用
infer
关键词得到了Fn
函数的参数类型以及返回类型,然后把新的参数添加到参数列表,并原样返回其函数类型。
Permutation(排列组合)
用法
Permutation
是用来将联合类型中的每一个类型进行排列组合,其用法如下:
实现方式
type Permutation<T, U = T> =
[T] extends [never]
? []
: T extends U
? [T, ...Permutation<Exclude<U, T>>]
: never
代码详解:
[T] extends [never]
:这段代码主要是为了处理联合类型为空的情况。T extends U
:这段代码主要是需要使用分布式条件类型这个知识点,当T extends U
成立时,在其后的判断语句中,T
代表当前迭代的类型。<Exclude<U, T>
:因为此时的T
代表当前迭代的类型,所以我们从原始联合类型中排除当前类型,然后递归调用Permutation
。当T
为A
时,递归调用Permutation<'B' | 'C'>
, 此时结果为['A']
+['B', 'C']
或['A']
+['C', 'B']
。
LengthOfString(字符串的长度)
用法
LengthOfString
是用来计算一个字符串长度的,其用法如下:
type result = LengthOfString<'Hello'> // 5
type LengthOfString<
S extends string,
T extends string[] = []
> = S extends `${infer Char}${infer R}`
? LengthOfString<R, [...T, Char]>
: T['length']
代码详解:
- 我们通过一个泛型的辅助数组来帮计算字符串的长度,在第一次符合条件时,将其第一个字符添加到数组中,在后续的递归过程中,如果不符合条件,直接返回
T['length']
,这个过程可以用如下代码表示:
// 第一次递归
const T = ['H'], S = 'hello', R = 'ello'
// 第二次递归
const T = ['H','e'], S = 'ello', R = 'llo'
// 第三次递归
const T = ['H','e','l'], S = 'llo', R = 'lo'
// 第四次递归
const T = ['H','e','l','l'], S = 'lo', R = 'o'
// 第五次递归
const T = ['H','e','l','l', 'o'], S = 'o', R = ''
Flatten(数组降维)
用法
// 结果:[1, 2, 3]
type result = Flatten<[1, 2, [3]]>
实现方式
type Flatten<
T extends any[]
> = T extends [infer L, ...infer R]
? L extends any[]
? [...Flatten<L>, ...Flatten<R>]
: [L, ...Flatten<R>]
: []
代码详解:Flatten
数组降维的主要思路是,遍历数组中的每一个元素,判断其是否为一个数组,如果是,则递归调用Flatten
,进行降维。
AppendToObject(对象添加新属性)
用法
AppendToObject
是用来向指定对象添加一个额外的属性(key/value
),其用法如下:
实现方式
type basicKeyType = string | number | symbol
type AppendToObject<T, K extends basicKeyType, V> = {
[P in keyof T | K]: P extends keyof T ? T[P] : V
}
代码详解:
basicKeyType
:在JavaScript
中,因为一个对象的属性只能是string
、number
或者symbol
这三种类型,所以我们限定K
必须满足此条件。keyof T | K
:这里表示keyof T
的联合类型和K
,组合成一个新的联合类型。
Absolute(绝对值)
用法
Absolute
是用来取一个数的绝对值的,其用法如下:
// 结果1:"531"
type result1 = Absolute<-531>
// 结果2:"9999"
type result2 = Absolute<9_999n>
实现方式
type NumberLike = number | string | bigint
type Absolute<T extends NumberLike> = `${T}` extends `-${infer N}` ? N : `${T}`
代码详解:
NumberLike
:我们认为'1'
和1
都是一个合法的数字,所以定义一个辅助的NumberList
联合类型。${T}
extends-${infer N}
:这里判断我们传递的数字是否为负数,如果是则直接取其正数部分,否则直接返回。
注意:这里说到的取绝对值,最后的结果之所以是一个字符串类型,是因为TS
对递归次数有限制。如果你想要真正的数字类型,可以考虑实现一个MakeArray
辅助方法,使用此方法可以将字符串类型的数字,转换成一个真正的数字类型,如下:
type MakeArray<N extends string, T extends any[] = []> =
N extends `${T['length']}`
? T
: MakeArray<N, [...T, 0]>
// 结果:3
type result = MakeArray<'3'>['length']
StringToArray(字符串转数组)
提示
此题不属于type-challenges类型挑战题
用法
// 结果:['h', 'e', 'l', 'l', 'o']
type result = StringToArray<'hello'>
实现方式
type StringToArray<
S extends string,
U extends any[] = []
> = S extends `${infer Char}${infer R}`
? StringToArray<R, [...U, Char]>
: U
代码详解:StringToArray
的实现主要是使用了递归的思想,它每次拿到字符串中一个字符,然后存入一个辅助数组中,当字符串为空时,直接返回这个辅助数组。
StringToUnion(字符串转联合类型)
用法
// 结果:'h' | 'e' | 'l' | 'l' | 'o'
type result = StringToUnion<'hello'>
实现方式
// way1: 递归思想
type StringToUnion<
S extends string
> = S extends `${infer Char}${infer R}`
? Char | StringToUnion<R>
: never
// way2: 借用StringToArray
type StringToUnion<S extends string> = StringToArray<S>[number]
代码详解:StringToArray<S>
返回的是一个数组,T[number]
表示对一个数组进行数字类型索引迭代,其迭代结果是每个元素组合成的一个联合类型。
Merge(类型合并)
用法
Merge
是用来合并两个类型,如果有重复的字段类型,则第二个的字段类型覆盖第一个的,其用法如下:
实现方式
type Merge<F, S> = {
[P in keyof F | keyof S]: P extends keyof S ? S[P] : P extends keyof F ? F[P] : never
}
代码详解:
keyof F | keyof S
:这段代码的含义是将F
和S
这两个对象的键组合成一个新的联合类型。P extends
:这里进行了两次extends
判断,其中第二次不能直接写成F[P]
,而应该多判断一次,当满足条件时才使用F[P]
,这是因为P
的类型判断无法作用于:
符号后面。
KebabCase(字符串转连字符)
KebabCase
是用来将驼峰形式字符串,转成连字符形式字符串的,其用法如下:
// 结果:foo-bar-baz
type result = KebabCase<'FooBarBaz'>
实现方式
type KebabCase<
S extends string
> = S extends `${infer S1}${infer S2}`
? S2 extends Uncapitalize<S2>
? `${Uncapitalize<S1>}${KebabCase<S2>}`
: `${Uncapitalize<S1>}-${KebabCase<S2>}`
: S
Diff(类型差异部分)
用法
Diff
是用来获取两个类型的不同部分的,其用法如下:
实现方式
type DiffKeys<T, U> = Exclude<keyof T | keyof U, keyof (T | U)>
type Diff<T, U> = {
[K in DiffKeys<T, U>]: K extends keyof T ? T[K] : K extends keyof U ? U[K] : never
}
代码详解:
keyof Foo | keyof Bar
:这段代码是把T
和U
中的所有属性组合成一个新的联合类型。keyof (T | U)
:这段代码是取T
和U
的公共属性。Exclude<K1, K2>
:这段代码主要是用来从K1
中排除K2
,带入以上例子也就是排除掉所有公共属性。Diff<T, U>
:在获取到DiffKeys
后,就可以迭代的方式获取到每个属性key
,它所对应的类型了。K extends keyof U
:额外再判断一次,是因为K
不能在三元表达式右侧使用。
AnyOf(数组元素真值判断)
用法
// 结果1:true
type result1 = AnyOf<[0, false, 0, { name: 'name' }]>
// 结果2:false
type result2 = AnyOf<[0, '', false, [], {}]>
实现方式
type FalsyType = 0 | '' | false | undefined | null | [] | { [key: string]: never }
type AnyOf<T extends readonly any[]> = T[number] extends FalsyType ? false : true
代码详解:因为我们就是要区分true/false
,所以我们把所有为false
的值全部列举出来,然后使用T[number]
索引迭代,依次去跟FalsyType
比较,其中{ [key: string]: never }
表示空对象{}
。
IsNever(是否是Never类型)
IsNever
是用来判断是否为never
类型,其用法如下:
// 结果1:false
type result1 = IsNever<undefined>
// 结果2:true
type result2 = IsNever<never>
// 结果3:false
type result3 = IsNever<never | string>
实现方式
export type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false
// way1: 类型数组
type IsNever<T> = T[] extends never[] ? true : false
// way2: 数组值
type IsNever<T> = [T] extends [never] ? true : false
// way3: 值比较
type IsNever<T> = Equal<T, never>
IsUnion(是否联合类型)
用法
IsUnion
是用来判断一个类型是否为联合类型的,其用法如下:
// 结果1:true
type result1 = IsUnion<string|number|boolean>
// 结果2:false
type result2 = IsUnion<string>
// 结果3:false
type result2 = IsUnion<never>
实现方式
代码详解:上面的实现虽然代码不多,但可能无法一下子就弄明白,为了更好的理解这种实现方式,我们来看如下两个案例分析:
// 案例一
type T = string | number
step1: string | number extends string | number
step2: string extends string | number => [number] extends [never] => true
step3: number extends string | number => [string] extends [never] => true
step4: true | true
result: true
// 案例二
type T = string
step1: string extends string
step2: [never] extends [never] => false
result: false
根据之前我们学到的分布式条件类型知识,T extends U
的时候,会把T
进行子类型分发。
如案例一的step3
、step4
,在分发后会把每次迭代的结果联合起来,组合成最终的结果。
ReplaceKeys(类型替换)
ReplaceKeys
是用来在一个类型中,使用指定的Y类型来替换已经存在的T类型的,其用法如下:
// 结果:{ id: number; name: boolean; }
type result = ReplaceKeys<{ id: number; name: string; }, 'name', { name: boolean; }>
实现方式
type ReplaceKeys<U, T, Y> = {
[P in keyof U]:
P extends T
? P extends keyof Y
? Y[P]
: never
: U[P]
}
RemoveIndexSignature(移除索引签名)
用法
RemoveIndexSignature
是用来移除一个类型中的索引签名的,其用法如下:
type Foo = {
[key: string]: any;
foo(): void;
}
// 结果:{ foo(): void; }
type result = RemoveIndexSignature<Foo>
实现方式
type CheckIndexSignature<T, P> = P extends T ? true : false
type RemoveIndexSignature<T, K = PropertyKey> = {
[P in keyof T as (CheckIndexSignature<P, K> extends false ? P : never)] : T[P]
}
CheckIndexSignature
:因为索引签名有一个特点,为string | number | symbol
,所以我们通过P extends T ? true : false
形式排除此索引签名。其原理如下:
type FooKeys = string | 'foo'
// 第一次迭代
example1: T = 'foo', P = string | number | symbol
step1: (string | number | symbol) extends 'foo' ? true : false
step2: (string extends 'foo' ? true : false) |
(number extends 'foo' ? true : false) |
(symbol extends 'foo' ? true : false) |
step3: false | false | false
step4: false
// 第二次迭代
example2: T = string, P = string | number | symbol
step1: (string | number | symbol) extends string ? true : false
step2: (string extends string ? true : false) |
(number extends string ? true : false) |
(symbol extends string ? true : false) |
step3: true | false | false
step4: true | false
step5: boolean
as xxx
:在之前的案例中,我们介绍过as
的用法,在这里有加工或再次断言的意思。在使用in
操作符进行迭代时,对每一个P
再使用CheckIndexSignature
加工一下,如果是索引签名,这里的结果为never
,为never
时表示跳过当前迭代,进而达到排除索引签名的目的。
// 第一次迭代 P = 'foo'
CheckIndexSignature<P, K> extends false ? P : never
=> false extends false ? P : never
=> P
// 第二次迭代 P = string
CheckIndexSignature<P, K> extends false ? P : never
=> boolean extends false ? P : never
=> never
// 最终结果
type result = { foo(): void; }
PercentageParser(百分比解析)
用法
PercentageParser
是用来解析百分比字符串的,其用法如下:
实现方式
type CheckPrefix<S extends string> = S extends '+' | '-' ? S : never
type CheckSuffix<S extends string> = S extends `${infer L}%` ? [L, '%'] : [S, '']
type PercentageParser<S extends string> =
S extends `${CheckPrefix<infer L>}${infer R}`
? [L, ...CheckSuffix<R>]
: ['', ...CheckSuffix<S>]
代码详解:
CheckPrefix
是用来处理百分比字符串前面的符号的,如果存在+
或者-
,则原样返回,如果不存在则返回never
,表示没有符号。CheckSuffix
是用来处理百分比字符串后面的百分比符号的,如果存在,则返回一个数组(最后一项固定为百分比符号);如果不存在,则返回的数组最后一个元素固定为空字符串。
DropChar(移除字符)
用法
// 结果:butterfly!
type result = DropChar<' b u t t e r f l y ! ', ' '>
实现方式
type DropChar<
S extends string,
C extends string
> = C extends ''
? S
: S extends `${infer L}${C}${infer R}`
? DropChar<`${L}${R}`, C>
: S
代码详解:DropChar
和ReplaceAll
的实现思路非常相似,首先需要判断待移除的字符是不是空字符串,如果是,则直接返回原始字符串;如果不是,先判断字符串中是否包含待移除的字符,包含则递归调用;不包含则直接返回原始字符串。
MinusOne(减一)
MinusOne
是用来实现数字减一的,其用法如下:
用法
实现方式
type MinusOne<
N extends number,
T extends any[] = []
> = N extends T['length']
? T extends [infer F, ...infer Rest]
? Rest['length']
: never
: MinusOne<N, [0, ...T]>
代码详解:在实现MinusOne
的时候,借用了一个空数组,首先判断数组的长度是否等于传递的数字N
,如果相等则从数组中随意移除一位,然后返回剩下数组的长度即可;如果不相等,则往数组中添加一个元素,再递归调用MinusOne
。
注意:由于TS
在递归调用时存在最大递归调用次数,所以对于比较大的数字会提示错误。
PickByType(根据类型选取)
用法
interface Model {
name: string
count: number
isReadonly: boolean
isEnable: boolean
}
// 结果:{ isReadonly: boolean, isEnable: boolean }
type result = PickByType<Model, boolean>
实现方式
type PickByType<T, U> = {
[P in keyof T as T[P] extends U ? P : never]: T[P]
}
代码详解:PickByType
的实现,可以使用as
进行第二次断言,当类型满足时就返回当前迭代的P
,不满足类型时就返回never
,因为never
最后会被排除,所以最后的迭代结果只有满足类型的键。
StartsWith(字符串startsWith方法)
StartsWith
是用来实现JavaScript
中字符串的startsWith
功能,其用法如下:
// 结果:true
type result = StartsWith<'abc', 'ab'>
实现方式
type StartsWith<
S extends string,
C extends string
> = S extends `${C}${string}` ? true : false
EndsWith(字符串endsWith方法)
EndsWith
是用来实现JavaScript
中字符串的endsWith
功能,其用法如下:
// 结果:true
type result = endsWith<'abc', 'bc'>
实现方式
type EndsWith<
S extends string,
C extends string
> = S extends `${string}${C}` ? true : false
PartialByKeys(按需可选)
用法
PartialByKeys
是用来实现按需可选的,其用法如下:
interface User {
name: string
age: number
address: string
}
interface UserPartialName {
name?: string,
age: number
address: string
}
// 结果:UserPartialName
type result = PartialByKeys<User, 'name'>
type CopyKeys<T> = {
[P in keyof T]: T[P]
}
type PartialByKeys<
T,
K extends keyof T = keyof T
> = CopyKeys<Omit<T, K> & {
[P in K]?: T[P]
}>
代码详解:
Omit
部分:根据之前介绍的Omit
的知识,Omit<T, K>
表示从T
中剔除含有K
的类型。CopyKeys
部分:如果不使用CopyKeys
,最后的结果为T & U
形式,它实际上与使用CopyKeys
的结果是一样的。这里使用CopyKeys
,很大程度上是为了测试。
// 使用CopyKeys,结果为true;不使用,结果为false
type result1 = Equal<PartialByKeys<User, 'name'>, UserPartialName>
RequiredByKeys(按需必填)
在实现PartialByKeys
后,很容易按照相同的思路去实现RequiredByKeys
。
用法
interface User {
name?: string
age?: number
address?: string
}
interface UserRequiredName {
name: string
age?: number
address?: string
}
// 结果:UserRequiredName
type result = RequiredByKeys<User, 'name'>
实现方式
type CopyKeys<T> = {
[P in keyof T]: T[P]
}
type RequiredByKeys<
T,
K extends keyof T = keyof T
> = CopyKeys<Omit<T, K> & {
[P in K]-?: T[P]
}>
代码详解:实现思路参考PartialByKeys
。
Mutable(可改)
用法
Mutable
是用来让所有属性变为可改的(移除readonly
关键词),其用法为:
实现方式
type MyMutable<T> = {
-readonly [P in keyof T]: T[P]
}
代码解读:
-readonly
:表示把readonly
关键词去掉,去掉之后此字段变为可改的。
OmitByType(按类型移除)
OmitByType
的实现思路和PickByType
类似。
用法
interface Model {
name: string
count: number
isReadonly: boolean
isEnable: boolean
}
interface ModelOmitBoolean {
name: string;
count: number
}
// 结果:ModelOmitBoolean
type result = OmitByType<Model, boolean>
实现方式
type OmitByType<T, U> = {
[P in keyof T as U extends T[P] ? never : P]: T[P]
}
代码解析:实现思路参考PickByType
。
ObjectEntries
用法
ObjectEntries
是用来实现JavaScript
中的Object.entries()
方法,其用法如下:
interface Model {
name: string;
age: number;
locations?: string[] | null;
}
type ModelEntries = ['name', string] | ['age', number] | ['locations', string[] | null];
// 结果:ModelEntries
type result = ObjectEntries<Model>
type RemoveUndefined<T> = [T] extends [undefined] ? T : Exclude<T, undefined>
type ObjectEntries<T> = {
[P in keyof T]-?: {} extends Pick<T, P> ? [P, RemoveUndefined<T[P]>] : [P, T[P]]
}[keyof T]
代码详解:
RemoveUndefined
:当T
仅为undefined
,表示原始类型就是undefined
; 当T
为联合类型时,移除联合类型中的undefined
。[P in keyof T]-?
: 表示移除可选属性。{} extends Pick<T, P>
: 判断当前的P
是否为可选属性,是的话就是使用RemoveUndefined
移除其中的undefined
,否则取原始类型。
type Person = {
name?: string
}
// 结果都为true
type result1 = {} extends Person ? true : false
type result2 = { name: string; } extends Person ? true : false
Shift(数组shift方法)
用法
// Shift结果:[2, 3]
type shiftResult = Shift<[1, 2, 3]>
实现方式
// Shift实现
type Shift<T extends any[]> = T extends [infer F, ...infer R] ? R : []
TupleToNestedObject(元组转嵌套对象)
用法
TupleToNestedObject
是用来将元组转成嵌套对象的,其用法如下:
// 结果:{ a: { b: string; } }
type result = TupleToNestedObject<['a', 'b'], string>
type TupleToNestedObject<T extends any[], U> =
T extends [infer F, ...infer R]
? { [P in F & string]: TupleToNestedObject<R, U> }
: U
代码详解:
F & string
: 等价于如下代码:
F & string = F extends string ? F : never
Reverse
Reverse
是用来实现数组的reverse()
方法的,其用法如下:
// 结果:['b', 'a']
type result = Reverse<['a', 'b']>
实现方式
type Reverse<T extends any[]> =
T extends [...infer R, infer L]
? [L, ...Reverse<R>]
: []
FlipArguments(反转函数参数类型)
借助上面的Reverse
方法,可以很容易实现函数参数的反转。
FlipArguments
是用来实现反转函数参数类型的,其用法如下:
// 结果:(a: number, b: string) => string | number
type result = FlipArguments<(a: string, b: number) => string | number>
实现方式
type FlipArguments<T extends (...args: any) => any> =
T extends (...args: infer A) => infer R
? (...args: Reverse<A>) => R
: never
FlattenDepth(数组按深度降维)
用法
// 结果:[1, 2, 3, 4, [5]]
type result = FlattenDepth<[1, 2, [3, 4], [[[5]]]], 2>
实现方式
type FlattenDepth<
T extends any[],
D extends number = 1,
U extends any[] = []
> = T extends [infer F, ...infer R]
? U['length'] extends D
? T
: F extends any[]
? [...FlattenDepth<F, D, [0, ...U]>, ...FlattenDepth<R, D>]
: [F, ...FlattenDepth<R, D, U>]
: T
代码详解:FlattenDepth
的实现思路和Flatten
基本一致,区别是按深度降维时需要一个数组去记录降维的次数(深度)。
BEM
用法
BEM
是用来将字符串连接成CSS BEM格式的,其用法如下:
// 结果:'btn__primary--small' | 'btn__primary--mini'
type result = BEM<'btn', ['primary'], ['small', 'mini']>
type ArrayToString<
T extends any[],
P extends string
> = T extends [] ? '' : `${P}${T[number]}`
type BEM<
B extends string,
E extends string[],
M extends string[]
> = `${B}${ArrayToString<E, '--'>}${ArrayToString<M, '__'>}`
代码详解:实现BEM
的思路并不复杂,只需要记住如下两个知识点:
- 判断是一个空数组,可以使用
T extends []
或者T['length'] extends 0
。 T[number]
会自动迭代数组,例如:
// 结果: 'A__B' | 'A__C' | 'A__D'
type result = `A__${['B', 'C', 'D'][number]}`
InOrderTraversal(中序遍历)
先序遍历:PreOrderTraversal
先访问根节点,然后访问左节点,最后访问右节点。
中序遍历:InOrderTraversal
先访问左节点,然后访问根节点,最后访问右节点。
后序遍历:PostOrderTraversal
先访问左节点,然后访问右节点,最后访问根节点。
用法
InOrderTraversal
是用来实现二叉树中序遍历的,其用法如下:
const tree = {
val: 1,
left: null,
right: {
val: 2,
left: {
val: 3,
left: null,
right: null,
},
right: null,
},
}
// 结果: [1, 3, 2]
type result = InOrderTraversal<typeof tree>
实现方式
代码详解:
[T] extends [TreeNode]
: 使用此形式而不用T extends TreeNode
,这是因为T
是一个TreeNode | null
,在左侧会进行分布式条件类型,判断两次:
// 如果Tree嵌套比较深的话,ts会报错
TreeNode extends TreeNode |
null extends TreeNode
- 遍历方式:根据先序遍历
PreOrderTraversal
、中序遍历InOrderTraversal
、后序遍历PostOrderTraversal
的定义,只需要在递归的时候处理其访问顺序即可。
FlipObject(对象键值交换)
FlipObject
是用来将对象的键值交换的,其用法如下:
// 结果:{ pi: 'a' }
type result = FlipObject<{ a: 'pi' }>
实现方式
type BasicType = string | number | boolean
type FlipObject<T extends Record<string, BasicType>> = {
[P in keyof T as `${T[P]}`]: P
}
Fibonacci(斐波那契数列)
菲波那切数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144...
用法
Fibonacci
是用来实现斐波那契数列的,用法如下:
实现方式
type Fibonacci<
N extends number,
Index extends any[] = [1],
Prev extends any[] = [],
Current extends any[] = [1]
> = Index['length'] extends N
? Current['length']
: Fibonacci<N, [...Index, 1], Current, [...Prev, ...Current]>
代码详解:
Index
:标记当前数列是第几项,从1开始。Prev
:存储数列上一次计算的值,从0开始。Current
: 标记当前数列的值,根据数列的特点,第N项的值,等于N - 1
项 +N - 2
项的值,即:Current = [...Prev, ...Current]
AllCombinations(全排列)
用法
AllCombinations
是用来列举全部排列组合可能性的,其用法如下:
// 结果:'' | 'A' | 'AB' | 'B' | 'BA'
type result = AllCombinations<'AB'>
实现方式
type StringToUnion<S extends string> =
S extends `${infer F}${infer R}`
? F | StringToUnion<R>
: never
type Combination<
S extends string,
U extends string = '',
K = S
> = [S] extends [never]
? U
: K extends S
? Combination<Exclude<S, K>, U | `${U}${K}`>
: U
type AllCombinations<S extends string> = Combination<StringToUnion<S>>
StringToUnion
是用来将字符串变成一个联合类型的,例如:
// 结果: 'A' | 'B'
type result = StringToUnion<'AB'>
Combination
是用来将联合类型进行排列组合的,以以上'A' | 'B'
这个联合类型为例,步骤如下:
// 第一步:从'A' | 'B这个联合类型中排除当前迭代的字符'A'
K = 'A' S = 'A' | 'B' => Exclude<'A' | 'B', 'A'>
// 第一步子递归:
Combination<'B', '' | 'A'> => '' | 'A' | `${'' | 'A'}B` => '' | 'A' | 'B' | 'AB'
// 第二步:从'A' | 'B'这个联合类型中排除当前迭代的字符'B'
K = 'B' S = 'A' | 'B' => Exclude<'A' | 'B', 'B'>
// 第二步子递归:
Combination<'A', '' | 'B'> => '' | 'B' | `${'' | 'B'}A` => '' | 'B' | 'A' | 'BA'
// 结果:剔除相同元素
result = '' | 'A' | 'AB' | 'B' | 'BA'
GreaterThan(大于)
用法
// 结果:true
type result = GreaterThan<2, 1>
实现方式
// 如果比较的数比较大,会提示:Type instantiation is excessively deep and possibly infinite
type GreaterThan<
T extends Number,
N extends Number,
R extends any[] = []
> = T extends R['length']
? false
: N extends R['length']
? true
: GreaterThan<T, N, [...R, 0]>
代码详解:使用一个空数组来辅助,每次递归添加一个元素,如果正整数T
先等于这个数组的长度,则为false
;如果正整数N
先等于这个数组的长度,则为true
。
Zip(按位置匹配)
Zip
是用来将两个元组按照相同索引位置组合成一个新数组的,用法如下:
// 结果:[[1, true], [2, false]]
type result = Zip<[1, 2], [true, false]>
实现方式
type Zip<
T extends readonly any[],
U extends readonly any[]
> = T extends [infer First, ...infer Rest]
? U extends [infer Head, ...infer Tail]
? [[First, Head], ...Zip<Rest, Tail>]
: []
: []
IsTuple(是否为元组)
用法
IsTuple
是用来判断是否为一个元组的,用法如下:
实现方式
type IsTuple<T> =
[T] extends [never]
? false
: T extends readonly any[]
? number extends T['length']
? false
: true
: false
代码解析:以上代码中,比较关键的代码是number extends T['length']
,这里不能写成T['length'] extends number
,如下:
// case1:需要返回false,因为它不定长,违反了元组的定义
type result1 = IsTuple<number[]>
// case2:需要返回true,因为它定长,只不过长度为0
type result2 = IsTuple<[]>
// case1计算逻辑,T['length']返回的是number,不是一个确定的值
number extends T['length']
=> number extends number
=> true
// case2计算逻辑,T['length']返回的是0
number extends T['length']
=> number extends 0
=> false
Chunk(lodash分割数组)
Lodash Chunk: 将一个数组分割成长度为N的多个小数组。
// 结果:[[1, 2], [3, 4]]
type result = Chunk<[1, 2, 3, 4], 2>
实现方式
type Chunk<
T extends any[],
Size extends number,
R extends any[] = []
> = R['length'] extends Size
? [R, ...Chunk<T, Size>]
: T extends [infer F, ...infer L]
? Chunk<L, Size, [...R, F]>
: R['length'] extends 0
? []
: [R]
代码详解:实现Chunk
大体思路是:借助一个辅助空数组,在遍历数组时往这个辅助数组中添加元素,一直到等于指定长度,然后进行下一次相同操作。
Fill(数组fill方法)
用法
Fill
是用来在一个数组中,用指定元素,替换开始索引和结束索引元素的。
// 结果:[1, true, true]
type result = Fill<[1, 2, 3], true, 1, 3>
type Fill<
T extends unknown[],
N extends number,
Start extends number = 0,
End extends number = T['length'],
Count extends any[] = [],
Flag extends boolean = Count['length'] extends Start ? true : false
> = Count['length'] extends End
? T
: T extends [infer F, ...infer L]
? Flag extends false
? [F, ...Fill<L, N, Start, End, [...Count, 0]>]
: [N, ...Fill<L, N, Start, End, [...Count, 0], true>]
: T
代码详解:
Count
: 遍历标志位,从数组第一项开始,当等于End
时,结束替换。Flag
:遍历标志位,从数组第一项开始,当等于Start
是,开始替换。
// 结果:[1, true, true]
type result = Fill<[1, 2, 3], true, 1, 3>
// 第一次遍历 Count = [], Flag = false, T = [1, 2, 3]
// 满足Flag extends false条件,Count = [0]
// 第二次遍历 Count = [0], Flag = true(计算而言),T = [1, 2, 3]
// 不满足Flag extends false条件,开始替换,Count = [0, 0], T = [1, true, 3]
// 第三次遍历 Count = [0, 0], Flag = true(主动传递), T =[1, true, 3]
// 不满足Flag extends false条件,开始替换,Count = [0, 0, 0], T = [1, true, true]
// 最后一次判断 Count = [0, 0, 0],长度等于End,结束,T = [1, true, true]
Without(移除)
用法
// 结果:[3]
type result = Without<[1, 2, 1, 2, 3], [1, 2]>
实现方式
type ToUnion<T> = T extends any[] ? T[number] : T
type Without<
T extends any[],
F,
U = ToUnion<F>,
R extends any[] = []
> = T extends [infer First, ...infer Rest]
? First extends U
? Without<Rest, F, U, [...R]>
: Without<Rest, F, U, [...R, First]>
: R
代码详解:因为F
支持单数字和数组,所以定义一个ToUion
来统一处理成联合类型。随后直接遍历数组,如果当前迭代的元素在联合类型中,则直接跳过进行下一次迭代;否则,把当前迭代元素添加到R
辅助数组中。
Trunc(Math.trunc取整)
Trunc
是用来实现Math.trunc()
方法的,其用法如下:
// 结果1:100
type result1 = Trunc<100.32>
// 结果2:0
type result2 = Trunc<.3>
实现方式
type Trunc<
T extends number | string
> =`${T}` extends `${infer L}.${string}`
? L extends ''
? '0'
: L
: `${T}`
IndexOf(数组indexOf方法)
用法
IndexOf
是用来实现数组indexOf
方法的,其用法如下:
实现方式
type IsEqual<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false
type IndexOf<
T extends any[],
U,
Index extends any[] = []
> = T extends [infer First, ...infer Rest]
? IsEqual<U, First> extends true
? Index['length']
: IndexOf<Rest, U, [...Index, 0]>
: -1
代码详解:需要借助IsEqual
来判断两个值是否相等,原因考虑如下案例:
type result1 = IsEqual<1, number> // false
type result2 = IsEqual<'a', string> // false
Join(数组join方法)
Join
是用来实现数组join
方法的,其用法如下:
// 结果:'a-p-p-l-e'
type result = Join<['a', 'p', 'p', 'l', 'e'], '-'>
实现方式
type Join<
T extends any[],
U extends string | number,
R extends string = ''
> = T extends [infer First, ...infer Rest]
? Rest['length'] extends 0
? `${R extends '' ? '' : `${R}${U}`}${First&string}`
: Join<Rest, U, `${R extends '' ? '' : `${R}${U}`}${First&string}`>
: R
LastIndexOf(数组lastIndexOf方法)
借助IndexOf
的实现思路,很容易实现lastIndexOf
方法。
LastIndexOf
是用来实现数组lastIndexOf
方法的,其用法如下:
// 结果:3
type result = LastIndexOf<[1, 2, 3, 4, 5], 4>
实现方式
type IsEqual<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false
type LastIndexOf<
T extends any[],
U
> = T extends [...infer Rest, infer Last]
? IsEqual<Last, U> extends true
? Rest['length']
: LastIndexOf<Rest, U>
: -1
Unique(数组去重)
用法
// 结果:[1, 2, 3]
type result = Unique<[1, 1, 2, 2, 3, 3]>
实现方式
借助IsEqual
和Includes
,很容易实现Unique
数组去重。
type IsEqual<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false
type Includes<
T extends any[],
U
> = T extends [infer First, ...infer Last]
? IsEqual<First, U> extends true
? true
: Includes<Last, U>
: false
type Unique<
T extends any[],
R extends any[] = []
> = T extends [infer First, ...infer Last]
? Includes<R, First> extends true
? Unique<Last, R>
: Unique<Last, [...R, First]>
: R
MapTypes(类型转换)
用法
MapTypes
是用来根据指定类型进行替换的,其用法如下:
// 结果:{ type: number; age: number; }
type result = MapTypes<{ type: string; age: number; }, { mapFrom: string;mapTo: number; }>
type GetMapType<
T,
R,
Type = R extends { mapFrom: T, mapTo: infer To } ? To : never
> = [Type] extends [never] ? T : Type
type MapTypes<T, R> = {
[P in keyof T]: GetMapType<T[P], R>
}
代码详解:在以上的实现中,最核心的代码是获取Type
类型。
R extends { mapFrom: T, mapTo: infer To }
:这段代码表示,R
是不是右边的子类型,我们以以上案例来说明:
// 当P = 'type'时,
T[P] = string, R = { mapFrom: string;mapTo: number; }
=> { mapFrom: string; mapTo: number; } extends { mapFrom: string, mapTo: infer To }
=> To = number
=> { type: number }
// 当P = 'age'时
T[P] = number, R = { mapFrom: string;mapTo: number; }
=> { mapFrom: string; mapTo: number; } extends { mapFrom: number, mapTo: infer To }
=> never
=> GetMapType<T[P], R> = number
=> { age: number }
ConstructTuple(构造元组)
ConstructTuple
是用来构造指定长度的元组的,其用法如下:
// 结果:[unknown, unknown]
type result = ConstructTuple<2>
实现方式
type ConstructTuple<
L extends number,
R extends any[] = []
> = R['length'] extends L
? R
: ConstructTuple<L, [...R, unknown]>
NumberRange(限定范围数字)
用法
// 结果:2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
type result = NumberRange<2, 9>
实现方式
实现思路参考:数组Fill
方法。
type NumberRange<
L,
H,
I extends any[] = [],
F = I['length'] extends L ? true : false
> = I['length'] extends H
? I[number] | H
: F extends false
? NumberRange<L, H, [...I, never]>
: NumberRange<L, H, [...I, I['length']], true>
Subsequence(元组子序列)
用法
Subsequence
是用来根据指定数组生成元组子序列的,其用法如下:
// 结果:[] | [1] | [2] | [1, 2]
type result = Subsequence<[1, 2]>
type Subsequence<
T extends any[],
R extends any[] = []
> = T extends [infer First, ...infer Last]
? Subsequence<Last, R | [...R, First]>
: R
代码详解:
R | [...R, First]
: 在一个数组中,对一个联合类型的数组使用...
扩展时,会自动进行元素分发,例如:
// 结果:['a', 'b'] | ['a', 'c']
type result = ['a', ...(['b'] | ['c'])]
CheckRepeatedChars(是否包含相同字符)
用法
CheckRepeatedChars
是用来检查字符串中是否存在重复字符的,其用法如下:
实现方式
type CheckRepeatedChars<
S extends string
> = S extends `${infer First}${infer Last}`
? Last extends `${string}${First}${string}`
? true
: CheckRepeatedChars<Last>
: false
代码详解:
${string}${First}${string}
:表示字符串包含First
,也可以用infer
来代替:${infer Left}${First}${infer Right}
。
FirstUniqueCharIndex(字符串中第一个唯一字符)
用法
FirstUniqueCharIndex
是用来获取字符串中第一个唯一字符的索引的,其用法如下:
// 结果1: 0(字符l)
type result1 = FirstUniqueCharIndex<'leetcode'>
// 结果2: 2(字符v)
type result2 = FirstUniqueCharIndex<'loveleetcode'>
借助CheckRepeatedChars
的实现思路,很容易实现FirstUniqueCharIndex
。
type FirstUniqueCharIndex<
S extends string,
R extends any[] = []
> = S extends ''
? -1
: S extends `${infer First}${infer Last}`
? First extends R[number]
? FirstUniqueCharIndex<Last, [...R, First]>
: Last extends `${string}${First}${string}`
? FirstUniqueCharIndex<Last, [...R, First]>
: R['length']
: -1
代码详解:此题的实现思路和FirstUniqueCharIndex
类似,只是多了一层判断,以上面案例为例:
// 结果: 2(字符v)
type result = FirstUniqueCharIndex<'loveleetcode'>
// 第一次迭代时:S = loveleetcode R = [] R[number] = never First = l
=> 'l' extends never 不满足,'oveleetcode' extends `${string}l${string}`满足
// 第二次迭代时:S = oveleetcode R = ['l'] R[number] = 'l' First = o
=> 'o' extends 'l' 不满足,'veleetcode' extends `${string}o${string}`满足
// 第三次迭代时:S = veleetcode R = ['l', '0'] R[number] = 'l' | 'o' First = v
=> 'v' extends 'l' | 'o' 不满足,'eleetcode' extends `${string}v${string}`不满足
// 结果:R['length']
=> 2
ParseUrlParams(解析url路径参数)
ParseUrlParams
是用来解析url
上参数名的,其用法如下:
// 结果:'id' | 'user'
type result = ParseUrlParams<'posts/:id/:user'>
实现方式
type ParseUrlParams<
S extends string
> = S extends `${string}:${infer Last}`
? Last extends `${infer Left}/${infer Right}`
? Left | ParseUrlParams<Right>
: Last
: never
GetMiddleElement(数组中位数)
用法
GetMiddleElement
是用来取数组中位数的,其用法如下:
实现方式
type GetMiddleElement<
T extends any[]
> = T['length'] extends 0 | 1 | 2
? T
: T extends [any, ...infer Middle, any]
? GetMiddleElement<Middle>
: never
代码详解:
T['length'] extends 0 | 1 | 2
:当数组长度小于等于而时,其中位数就是自身。T extends [any, ...infer Middle, any]
: 当长度大于2时,每次迭代去掉首、尾元素,直至数组长度小于等于2,返回。
FindOnlyElements(数组只出现一次的元素)
FindOnlyElements
是用来获取数组中只出现一次的元素,其用法如下:
// 结果1: [1, 2, 3]
type result1 = FindOnlyElements<[1, 2, 3]>
// 结果2: [1]
type result2 = FindOnlyElements<[1, 2, 3, 2, 3]>
实现方式
type FindOnlyElements<
T extends any[],
U extends any[] = [],
R extends any[] = []
> = T extends [infer First, ...infer Last]
? First extends [...U, ...Last][number]
? FindOnlyElements<Last, [...U, First], R>
: FindOnlyElements<Last, [...U, First], [...R, First]>
: R
CountArrayElement(计数数组中元素出现的次数)
用法
CountArrayElement
是用来实现计算数组中元素出现次数的,其用法如下:
实现方式
type Flatten<
T extends any[],
R extends any[] = []
> = T extends [infer First, ...infer Last]
? [First] extends [never]
? Flatten<Last, R>
: First extends any[]
? Flatten<Last, [...R, ...Flatten<First>]>
: Flatten<Last, [...R, First]>
: R
type ObjectCount<
T extends any[],
R extends Record<string | number, any[]> = {}
> = T extends [infer First extends string | number, ...infer Last]
? First extends keyof R
? ObjectCount<Last, Omit<R, First> & Record<First, [...R[First], 0]>>
: ObjectCount<Last, Omit<R, First> & Record<First, [0]>>
: {
[P in keyof R]: R[P]['length']
}
type CountArrayElement<
T extends any[]
> = ObjectCount<Flatten<T>>
代码详解:
Flatten
:实现Flatten
,用来处理传递多维数组的情况,例如:CountArrayElement<[1, [1, 2], 3, [4, [5]]]>
First extends keyof R
: 如果当前数组的遍历项是R
对象中的一个键,则表明需要计数加一;如果不是,则代表是新项,需要计数为1;[P in keyof R]: R[P]['length']
: 因为最后结果需要返回数组,而非数组,所以迭代R
对象,返回其每个属性的数组长度即可。
Integer(数字整数)
用法
Integer
是用来返回数字的整数部分的,如果传入的数子包含小数,则返回never
,其用法如下:
// 结果1:1
type result1 = Integer<1>
// 结果2:never
type result1 = Integer<1.1>
实现方式
type Integer<T extends number> = `${T}` extends `${bigint}` ? T : never
代码详解:
${T}
extends${bigint}
:这里转成字符串形式比较,不能直接比较,因为number
和bigint
是两个不同的类型。
// 结果:都是false
type result1 = number extends bigint ? true : false
type result2 = bigint extends number ? true : false
ToPrimitive(转化基本类型)
用法
ToPrimitive
是用来返回一个对象的类型的,其用法如下:
type PersonInfo = {
name: 'Tom'
age: 30
married: false
addr: {
home: '123456'
phone: '13111111111'
}
hobbies: ['sing', 'dance']
readonlyArr: readonly ['test']
fn: () => any
}
type Expected = {
name: string
age: number
married: boolean
addr: {
home: string
phone: string
}
hobbies: [string, string]
readonlyArr: readonly [string]
fn: Function
}
// 结果:Expected
type result = ToPrimitive<PersonInfo>
type ToPrimitive<
T
> = T extends object
? T extends (...args: any[]) => any
? Function
: { [P in keyof T]: ToPrimitive<T[P]> }
: T extends { valueOf: () => infer R } ? R : T
代码详解:
- 对于函数来说:当满足
T extends (...args: any[]) => any
条件时,直接返回Function
。 - 对于嵌套对象来说,递归调用
ToPrimitive
即可。 - 对于普通类型来说,判断其是否满足
T extends { valueOf: () => infer R }
,是则返回其类型。
// ts中的valueOf是js中的valueOf一样
const num = 123
console.log(num.valueOf()) // 123
DeepMutable(深度Mutable)
DeepMutable
是用来深度移除属性readonly
修饰符的,其用法如下:
interface Test {
readonly title: string
readonly description: string
readonly completed: boolean
readonly meta: {
readonly author: string
}
}
interface Expected {
title: string
description: string
completed: boolean
meta: {
author: string
}
}
// 结果:Expected
type result = DeepMutable<Test>
实现方式
type DeepMutable<
T
> = T extends (...args: any[]) => any
? T
: { - readonly [P in keyof T]: DeepMutable<T[P]> }
AllMatch(数组元素是否与给定元素完全相同)
AllMatch
是用来判断,数组元素是否与给定元素完全相同的,其用法如下:
// 结果1:true
type result1 = AllMatch<[1, 1, 1], 1>
// 结果2:false
type result2 = AllMatch<[1, 1, 2], 1>
实现方式
// 不考虑边界情况,简易实现方法
type errTest1 = AllMatch<[any], unknown> // false
type errTest2 = AllMatch<[unknown], any> // false
type errTest3 = AllMatch<[1, 2], 1 | 2> // false
type AllMatch<
T extends any[],
U
> = T[number] extends U
? true
: false;
// 考虑边界情况:完整实现
type IsEqual<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false
type AllMatch<
T extends any[],
U
> = T extends [infer First, ...infer Rest]
? IsEqual<First, U> extends true
? AllMatch<Rest, U>
: false
: true
Filter(数组过滤)
Filter
是用来实现数组过滤方法的,其用法如下:
// 结果1:[2]
type result1 = Filter<[0, 1, 2], 2>
// 结果2:[1, 2]
type result2 = Filter<[0, 1, 2], 1 | 2>
实现方式
type Filter<
T extends any[],
P
> = T extends [infer First, ...infer Rest]
? First extends P
? [First, ...Filter<Rest, P>]
: Filter<Rest, P>
: []
FindAllIndex(查找数组中给定元素所有索引)
用法
FindAllIndex
是用来返回字符串中所有匹配索引的,其用法如下:
实现方式
type FindAll<
T extends string,
P extends string,
R extends any[] = [],
I extends any[] = []
> = P extends ''
? []
: T extends `${string}${infer Last}`
? T extends `${P}${string}`
? FindAll<Last, P, [...R, I['length']], [...I, 0]>
: FindAll<Last, P, R, [...I, 0]>
: R
代码详解:
I extends any[] = []
: 设置索引,字符串每迭代移除,I
数组长度增加一。T extends ${P}${string}
: 当满足条件时,向结果数组R
中添加当前索引即可。
CombKeys(组合键)
CombKeys
是用来实现组合键的,其用法如下:
// 结果:'cmd ctrl' | 'cmd opt' | 'cmd fn' | 'ctrl opt' | 'ctrl fn' | 'opt fn'
type result = CombKeys<['cmd', 'ctrl', 'opt', 'fn']>
实现方式
type CombKeys<
T extends any[]
> = T extends [infer First extends string, ...infer Last extends string[]]
? `${First} ${Last[number]}` | CombKeys<Last>
: never
ReplaceFirst(替换元组中第一个匹配项)
用法
// 结果:[1, 2, 4]
type result = ReplaceFirst<[1, 2, 3], 3, 4>
实现方式
type ReplaceFirst<
T extends readonly unknown[],
From,
To
> = T extends [infer First, ...infer Rest]
? First extends From
? [To, ...Rest]
: [First, ...ReplaceFirst<Rest, From, To>]
: T
SimpleVue(简单Vue类型)
用法
实现方式
Currying(柯里化)
在JavaScript
中Currying
是用来实现函数柯里化的,其用法如下:
const add = (a: number, b: number) => a + b
const three = add(1, 2)
const curriedAdd = Currying(add)
const five = curriedAdd(2)(3)
用法
Currying
是用来实现JavaScript
中的柯里化的,其用法如下:
实现方式
type CurryFunction<
P extends any[],
R
> = P extends []
? () => R
: P extends [infer First, ...infer Rest]
? Rest['length'] extends 0
? (a: First) => R
: (a: First) => CurryFunction<Rest, R>
: R
declare function Currying<F>(fn: F):
F extends (...args: infer P) => infer R
? CurryFunction<P, R>
: never
代码详解:
P
:P
为调用Currying
函数时传递函数参数的参数数组,以上面为例,其值为:[number, string, boolean]
。P extends [infer First, ...infer Rest]
: 遍历参数列表,依次返回一个函数即可。
UnionToIntersection(元组取交集)
在实现UnionToIntersection
之前,我们先来回顾一下TS
中&
符号的作用:
// 结果:never
type result1 = 1 & 'foo' & true
// 结果:{ a: number; b: number; c: boolean; }
type result2 = { a: number; b: number; } & { b: string | number; c: boolean; }
// 结果:(a: boolean | number) => string
type result3 = ((a: boolean) => string | number) & ((a: number) => string)
案例解析:
- 案例一:因为
1
、foo
以及true
,没有交集部分,所以这里结果为never
。 - 案例二:对于
a
和c
属性而言,它们只存在于自身类型,所以交集部分是自身;对于b
属性而言,它在两个类型中都存在,且其属性的类型存在交集部分,既:number
。 - 案例三:对于函数的交叉类型,我们从函数参数、函数返回值这两个部分来说明。对于函数参数而言,取其联合类型;对于函数返回值而言,取其交叉类型。
从以上几个案例中可以看出,TS
中的&
符号是取交集的意思,也叫交叉类型。
用法
UnionToIntersection
所做的事情和&
符号是一样的,其用法如下:
实现方式
type UnionToIntersection<U> =
(U extends any
? (x: U) => any
: never
) extends (x: infer V) => any
? V
: never
代码详解:
U extends any ? X : Y
: 这里把U
类型处理成(x: U) => any
的函数类型。T extends (x: infer V) => any ? V : never
:这里的T
就是上一步的函数类型,如果extends
成立,则返回V
,此时的V
必然满足U & V
。
RequiredKeys(所有必填字段)
用法
RequiredKeys
是用来返回一个类型中所有必填字段,其用法如下:
type Person = {
name: string;
age: number;
sex?: undefined;
address?: string;
}
// 结果:'name' | 'age'
type result = RequiredKeys<Person>
type RequiredKeys<T> = keyof {
[P in keyof T as ({} extends Pick<T, P> ? never : P)]: P
}
代码详解:
{} extends Pick<T, P> ? never : P
:是用来判断当前遍历键是否可选键的。
// never
type result = {} extends {} | { sex: undefined } ? never : 'sex'
GetRequired(必填字段组成的类型)
用法
GetRequired
是用来取一个类型中那些由必填字段组成的一个新类型的,其用法如下:
type Person = {
name?: string;
age: number;
address?: string;
sex: undefined;
}
// 结果:{ age: number; sex: undefined; }
type result = GetRequired<Person>
实现方式
type GetRequired<T> = {
[P in keyof T as (T[P] extends Required<T>[P] ? P : never)]: T[P]
}
代码详解:
T[P] extends Required<T>[P] ? P : never
:用来判断当前遍历键的类型是否一致,一致则是必填类型。
// P为name时
type result1 = string | undefined extends string ? 'name' : never
// P为age时
type result2 = number extends number ? 'age' : never
OptionalKeys(所有可选字段)
OptionalKeys
和RequiredKeys
所做的事情相反,其获取的是所有可选字段。
type Person = {
name: string;
age: number;
sex?: undefined;
address?: string;
}
// 结果:'sex' | 'address'
type result = OptionalKeys<Person>
实现方式
type OptionalKeys<T> = keyof {
[P in keyof T as ({} extends Pick<T, P> ? P : never)]: P
}
代码详解:从上面代码中可以看出,它和RequiredKeys
实现思路是一样的,区别只是在extends
关键词后面的处理不同。
GetOptional(可选字段组成的类型)
按照OptionalKeys
的实现思路,能够很容易的实现GetOptional
。
type Person = {
name: string;
age: number;
sex?: undefined;
address?: string;
}
// 结果:{ sex?: undefined; address?: string; }
type result = GetOptional<Person>
实现方式
type GetOptional<T> = {
[P in keyof T as (T[P] extends Required<T>[P] ? never : P)]: T[P]
}
CapitalizeWords(所有单词首字母大写)
用法
CapitalizeWords
是用来把一个字符串中所有单词,变为大写字母的,其中这个字符串以固定的分隔符分割,用法如下:
// 结果:'Foobar'
type t1 = CapitalizeWords<'foobar'>
// 结果:'Foo Bar.Hello,World'
type t2 = CapitalizeWords<'foo bar.hello,world'>
type CapitalizeWords<
S extends string,
R extends string = ''
> = S extends `${infer First}${infer Rest}`
? Uppercase<First> extends Lowercase<First>
? `${Capitalize<`${R}${First}`>}${CapitalizeWords<Rest>}`
: CapitalizeWords<Rest, `${R}${First}`>
: Capitalize<R>
代码详解:
Uppercase<First> extends Lowercase<First>
:为了找到连串的大写字符串,例如:
// S = foo bar.hello,world
R = 'foo' First = ' ' Rest = 'bar.hello,world'
=> `${Capitalize<`foo `>}${CapitalizeWords<'bar.hello,world'>}`
=> `Foo ${CapitalizeWords<'bar.hello,world'}`
=> ...
=> 'Foo Bar.Hello,World'
CamelCase(下划线字符串转小驼峰)
用法
与中级章节实现不同,此章节中CamelCase
是用来将下划线字符串转小驼峰的,其用法如下:
// 结果:'fooBarHelloWorld'
type result = CamelCase<'foo_bar_hello_world'>
实现方式
代码详解:
IsLetter
: 用来判断是否为字母的。
type result1 = IsLetter<'$'> // false
type result2 = IsLetter<'A'> // true
IsLetter<L> extends true
: 如果是字母的话,则根据是否以下划线结尾,如果是,则只需要紧邻下划线的字母L
大写即可,否小写。
ParsePrintFormat(获取字符串格式化参数)
用法
ParsePrintFormat
是用来获取字符串格式化参数的,其用法如下:
实现方式
type ControlMap = {
'c': 'char',
's': 'string',
'd': 'dec',
'o': 'oct',
'h': 'hex',
'f': 'float',
'p': 'pointer'
}
// way1: 借助辅助数组
type ParsePrintFormat<
S extends string,
R extends string[] = []
> = S extends `${infer S1}%${infer Char}${infer S2}`
? Char extends keyof ControlMap
? ParsePrintFormat<S2, [...R, ControlMap[Char]]>
: ParsePrintFormat<S2, R>
: R
// way2: 不借助辅助数组
type ParsePrintFormat<
S extends string
> = S extends `${string}%${infer Char}${infer Rest}`
? Char extends keyof ControlsMap
? [ControlsMap[Char], ...ParsePrintFormat<Rest>]
: ParsePrintFormat<Rest>
: []
代码详解:在以上实现方法中,借用了辅助数组的思想,拿上面案例来说,具体迭代分析如下:
VueBasicProps(Vue的Props类型)
用法
实现方式
IsAny和NotAny
用法
IsAny
是用来判断一个类型是否为any
的,NotAny
和它做的事情相反。
实现方式
type IsAny<T> = 0 extends (1 & T) ? true : false
type NotAny<T> = true extends IsAny<T> ? false : true
代码详解:1 & T
的结果只能是:1
、never
或者any
。当使用0 extends
这三个结果的时候,只有any
判断为真。
// 结果:false
type t1 = 0 extends 1 ? true : false
// 结果:false
type t2 = 0 extends never ? true : false
// 结果:true
type t3 = 0 extends any ? true : false
Get(字符串路径取值)
用法
Get
是用来进行字符串路径取值的,其用法如下:
type Data = {
foo: {
bar: {
value: 'foobar',
count: 6,
},
include: true,
},
'foo.baz': false
hello: 'world'
}
// 结果:world
type t1 = Get<Data, 'hello'>
// 结果:foobar
type t2 = Get<Data, 'foo.bar.value'>
// 结果:false
type t3 = Get<Data, 'foo.baz'>
// 结果:never
type t4 = Get<Data, 'no.exits'>
实现方式
type Get<
T,
K extends string
> = K extends keyof T
? T[K]
: K extends `${infer S1}.${infer S2}`
? Get<T[S1 & keyof T], S2>
: T[K & keyof T]
- 含有
.
符号的字符串:对于这种情况,我们先判断.
符号左侧部分是否满足为T
类型的某个key
,如果满足,则递归调用Get
;如果不满足,则直接返回never
。
// S1如果是T的属性键,则返回S1;如果不是,则返回never
Get<T[S1 & keyof T], S2>
// 等价于
S1 extends keyof T ? Get<T[S1], S2> : never
- 不含有
.
符号的字符串:对于这种情况,我们只需要判断它是否为T
类型中的某个key
,如果是,则直接取值;如果不是,则返回never
。
// K如果是T的属性键,则返回K;如果不是,则返回never
T[K & keyof T]
// 等价于
S extends keyof T ? T[S] : never
StringToNumber(字符串数字转数字)
用法
StringToNumber
是用来将字符串形式的数字转换成真正数字类型数字的,其用法如下:
// 结果:123
type result = StringToNumber<'123'>
实现方式
在JavaScript
中,我们可以很方便的调用Number()
方法或者parseInt()
方法来将字符串类型的数字,转换成数字类型的数字。但在TS
中,并没有这样的方法,需要我们来手动实现。
StringToNumber
的实现并不容易理解,我们需要将其进行拆分,一步步来完善,其实现思路如下:
- 第一步:可以很容易获取字符串
'123'
中每一位字符,我们将其存储在辅助数组T
中,如下:
type StringToNumber<S extends string, T extends any[] = []> =
S extends `${infer S1}${infer S2}`
? StringToNumber<S2, [...T, S1]>
: T
// 结果:['1', '2', '3']
type result = StringToNumber<'123'>
- 第二步:我们需要将单个字符串类型的数字,转换成真正数字类型的数字,可以借助中间数组来帮忙,例如:
'1' => [0]['length'] => 1
'2' => [0,0]['length'] => 2
'3' => [0,0,0]['length'] = 3
...
'9' => [0,0,0,0,0,0,0,0,0]['length'] => 9
根据以上规律,我们封装一个MakeArray
方法,它的实现代码如下:
type MakeArray<N extends string, T extends any[] = []> = N extends `${T['length']}` ? T : MakeArray<N, [...T, 0]>
type t1 = MakeArray<'1'> // [0]
type t2 = MakeArray<'2'> // [0, 0]
type t3 = MakeArray<'3'> // [0, 0, 0]
- 第三步:现在有了百位,十位和个位的数字,我们应该运用算术把它们按照一定的规律累加起来,如下:
const arr = [1, 2, 3]
let target = 0
// 第一次迭代
target = 10 * 0 + 1 = 1
// 第二次迭代
target = 10 * 1 + 2 = 12
// 第三次迭代
target = 10 * 12 + 3 = 123
// 迭代规律
target = 10 * target + N
根据以上思路,我们还需要一个乘十的工具函数,对应到实际需求,就是需要把一个数组copy
十次,因此我们封装一个Multiply10
工具,其实现代码如下:
- 第四步:根据前几步的分析,把所有东西串联起来,
StringToNumber
完整实现代码如下:
type Digital = '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'
type Multiply10<T extends any[]> = [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T]
type MakeArray<N extends string, T extends any[] = []> = N extends `${T['length']}` ? T : MakeArray<N, [...T, 0]>
type StringToNumber<S extends string, T extends any[] = []> =
S extends `${infer S1}${infer S2}`
? S1 extends Digital
? StringToNumber<S2, [...Multiply10<T>, ...MakeArray<S1>]>
: never
: T['length']
- 第五步:为了更好的理解递归的过程,我们拆解成如下步骤来说明:
type result = StringToNumber<'123'>
// 第一次递归,S满足${infer S1}${infer S2}, S1满足Digital
S = '123' S1 = '1' S2 = '23' T = [0] T['length'] = 1
// 第二次递归,S满足${infer S1}${infer S2}, S1满足Digital
S = '23' S1 = '2' S2 = '3' T = [0,....0] T['length'] = 10
// 第三次递归,S满足${infer S1}${infer S2}, S1满足Digital
S = '3' S1 = '3' S2 = '' T = [0,....0] T['length'] = 120
// 第四次递归,S不满足${infer S1}${infer S2} T['length']取值
S = '' T = [0,....0] T['length'] = 123
// 结果:
type result = StringToNumber<'123'> // 123
FilterOut(数组元素过滤)
用法
FilterOut
是用来从数组中移除指定元素的,其用法如下:
实现方式
type FilterOut<
T extends any[],
F,
K extends any[] = []
> = T extends [infer R, ...infer args]
? [R] extends [F]
? FilterOut<args, F, [...K]>
: FilterOut<args, F, [...K, R]>
: K
代码详解:
- 第一步:我们借用赋值函数来存放最后的结果。
- 第二步:迭代数组
T
,拿每一个元素去和指定的F
进行判断,如果R
是F
的子类型,则不添加此元素到结果数组中,反之添加。 - 第三步:当迭代完毕时,直接返回结果数组
K
。
TupleToEnum(元组转枚举)
用法
TupleToEnum
是用来将元组转换为枚举的,其用法如下:
const OperatingSystem = ['macOs', 'Windows', 'Linux'] as const
type Expected1 = {
readonly MacOs: 'macOs';
readonly Windows: 'Windows';
readonly Linux: 'Linux'
}
type Expected2 = {
readonly MacOs: 0;
readonly Windows: 1;
readonly Linux: 2
}
// 结果:Expected1
type result1 = TupleToEnum<typeof OperatingSystem>
// 结果:Expected2
type result2 = TupleToEnum<typeof OperatingSystem, true>
在实现TupleToEnum
之前,我们先来实现TupleKeys
,它是用来获取所有元组索引组合成的联合类型的。
type TupleKeys<
T extends readonly any[]
> = T extends readonly [infer R, ...infer args]
? TupleKeys<args> | args['length']
: never
// 结果:0 | 1 | 2
type keys = TupleKeys<typeof OperatingSystem>
在有了以上keys
后,就能很容易实现TupleToEnum
了,如下:
type TupleToEnum<
T extends readonly string[],
N extends boolean = false
> = {
readonly [K in TupleKeys<T> as Capitalize<T[K]>]: N extends true ? K : T[K]
}
Format(字符串格式化函数类型)
%s
表示格式化为(x: string) => any
形式,%d
表示格式化为(x: number) => any
形式。
Format
是将字符串格式化为指定函数类型的,用法如下:
// 结果1:(x: string) => string
type result1 = Format<'a%sbc'>
// 结果2:(x: number) => string
type result2 = Format<'a%dbc'>
// 结果3:(x: number) => (x: string) => string>
type result3 = Format<'a%dbc%s'>
实现方式
type FormatMaps = {
's': string;
'd': number;
}
type Format<
S extends string
> = S extends `${infer S1}%${infer P}${infer S2}`
? P extends keyof FormatMaps
? (x: FormatMaps[P]) => Format<S2>
: string
: string
LengthOfString(字符串的长度)
我们之前在中级大章节中已经实现过LengthOfString
,但它面临的问题是,如果字符有上百个,由于TS
对于递归的次数存在限制,会提示嵌套过深。
用法
实现方式
type LengthOfString<
S extends string,
R extends any[] = []
> = S extends `${infer S0}${infer S1}${infer S2}${infer S3}${infer S4}${infer S5}${infer S6}${infer S7}${infer S8}${infer S9}${infer Rest}`
? LengthOfString<Rest, [...R, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]>
: S extends `${infer S1}${infer S2}`
? LengthOfString<S2, [...R, S1]>
: R['length']
代码解析:这里我们巧妙的使用占位的思想,S extends ${infer S1}${infer S2}${infer S3}
,如果S
满足这个占位形式,则表示S
的长度至少为2
,带入到上面的例子,解析步骤如下:
// 第一次递归
S满足至少10个字符的长度,R = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
// 第二弟递归
S满足至少10个字符的长度,R = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
// 省略
// 最后一次递归
S = '1'不满足至少10个字符的长度,R = [1, ....., 1]
// 最后结果
R['length'] = 91
UnionToTuple(联合类型转元组)
用法
UnionToTuple
是用来将联合类型转成元组的,用法如下:
// 结果1:['a', 'b']
type result1 = UnionToTuple<'a'>
// 结果2:['a', 'b']
type result2 = UnionToTuple<'a' | 'b'>
// 结果3:['a', 'b']
type result3 = UnionToTuple<'a' | 'b' | never>
实现方式
type UnionToIntersection<U> = (
U extends any
? (x: U) => any
: never
) extends (x: infer R) => any
? R
: never
type LastUnion<U> = UnionToIntersection<
U extends any
? (x: U) => 0
: never
> extends (x: infer R) => 0
? R
: never
type UnionToTuple<
T,
Last = LastUnion<T>
> = [T] extends [never]
? []
: [...UnionToTuple<Exclude<T, Last>>, Last]
代码详解:
type f1 = (x: 1) => 0
type f2 = (x: 2) => 0
// 函数重载结果
// function (x: 1): 0;
// function (x: 2): 0;
type result = UnionToIntersection<f1 | f2>
对于函数参数的交集而言,不是简单的把参数取交集,而是"联合"起来,也就是构造一个新的函数类型,即:函数重载
LastUnion
: 取联合类型最后的一个元素,如果一个函数存在重载的情况,TS
会取最后一个函数签名,例如:
type f1 = (x: 1) => 0
type f2 = (x: 2) => 0
// 结果1:2
type result1 = f1 & f2 extends (x: infer R) => 0 ? R : never
// 结果2:1
type result2 = f2 & f1 extends (x: infer R) => 0 ? R : never
Join(字符串拼接)
Join
是用来实现拼接字符串的,用法如下:
// 结果1: ''
const Expected1 = join('-')();
// 结果2: 'a'
const Expected2 = join('-')('a');
// 结果3: 'abc'
const Expected3 = join('')('a', 'b', 'c');
// 结果4: 'a-b-c'
const Expected4 = join('-')('a', 'b', 'c');
实现方式
type Tail<T extends string[]> = T extends [any, ...infer Rest] ? Rest : []
type StringJoin<
D extends string,
P extends string[] = []
> = P extends []
? ''
: P extends [infer Only]
? Only
: `${P[0]}${D}${StringJoin<D, Tail<P>>}`
declare function join<D extends string>(delimiter: D): <P extends string[] = []>(...parts: P) => StringJoin<D, P>;
DeepPick(深层次Pick)
用法
type Obj = {
a: number,
b: string,
c: boolean,
obj: {
d: number,
e: string,
f: boolean,
obj2: {
g: number,
h: string,
i: boolean,
}
}
}
// 结果1:Obj
type result1 = DeepPick<Obj, ''>
// 结果2:{ a: number; }
type result2 = DeepPick<Obj, 'a'>
// 结果3:{ a: number; } & { obj: { d: number; } }
type result3 = DeepPick<Obj, 'a', 'obj.d'>
实现方式
在之前,我们实现过根据属性路径取值Get
,根据其思路我们很容易实现DeepPick
,如下:
type UnionToIntersection<U> =
(U extends any
? (x: U) => any
: never
) extends (x: infer V) => any
? V
: never
type GetType<T, S> =
S extends `${infer S1}.${infer S2}`
? S1 extends keyof T
? { [K in S1]: GetType<T[S1], S2> }
: never
: S extends keyof T
? { [K in S]: T[K] }
: never
type DeepPick<
T,
U extends string
> = UnionToIntersection<
U extends infer keys ? GetType<T, keys> : never
>
Camelize(对象属性键转小驼峰)
用法
Camelize
是用来将对象中的key
全部转换为小驼峰的,用法如下:
实现方式
type CamelCase<S> =
S extends `${infer S1}_${infer S2}`
? `${Lowercase<S1>}${CamelCase<Capitalize<Lowercase<S2>>>}`
: S
type Camelize<T> = {
[K in keyof T as CamelCase<K>]:
T[K] extends [infer R]
? [Camelize<R>]
: T[K] extends Object
? Camelize<T[K]>
: T[K]
}
代码详解:CamelCase
的实现可以分为两个部分,第一部分来自于处理属性key
转小驼峰的情况,第二部分来自于嵌套对象的情况。
- 处理属性
key
:根据之前介绍过的as
用法,我们可以在in
迭代过程中使用as
来进一步加工或者处理属性key
,也就是CamelCase
的部分。 - 处理嵌套对象:对于
T[P]
而言,我们考虑嵌套对象为数组和普通对象的情况,首先判断是否为数组类型,如果是则迭代数组递归调用Camelize
;如果是普通对象,则直接调用Camelize
;如果都不是,则直接返回T[P]
即可。
DropString(移除全部字符)
用法
// 结果:'ooar!'
type result = DropString<'foobar!', 'fb'>
实现方式
type StrngToUnion<S extends string> =
S extends `${infer S1}${infer S2}`
? S1 | StrngToUnion<S2>
: S
type DropString<
S extends string,
R extends string,
U = StrngToUnion<R>
> = S extends `${infer S1}${infer S2}`
? S1 extends U
? DropString<S2, R>
: `${S1}${DropString<S2, R>}`
: S
代码详解:实现DropString
的核心是将指定的字符串转换为联合类型,转换之后只需要迭代字符串,判断当前迭代的字符是不是在联合类型中,如果是则直接丢弃,不是则原样保留。
Split(字符串Split方法)
Split
是用来实现字符串split
方法的,其用法如下:
// 结果:["Hi!", "How", "are", "you?"]
type result = Split<'Hi! How are you?', ' '>
实现方式
ClassPublicKeys(类的公共键)
用法
实现方式
IsRequiredKeys(是否为必填key)
用法
IsRequiredKeys
是用来判断是否为必填key
的,其用法如下:
实现方式
type IsRequiredKey<T, K extends keyof T> = T extends Record<K, T[K]> ? true : false
根据IsRequiredKey
的实现思路,我们可以很容易实现IsOptionalKey
,如下: