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
类型挑战时弄清楚所有涉及到的知识点。
核心知识点
加号和减号
TIP
加号和减号的用法类似。
在一些内置工具中,可能会出现+
或者-
这些符号,例如:
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(必填)
TIP
此题不属于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(构造)
TIP
此题不属于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(交集)
TIP
此题不属于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:用法
Trim
、TrimLeft
以及TrimRight
这几个工具比较好理解,它们都是用来移除字符串中的空白符的。
type t1 = TrimLeft<' str'> // 'str'
type t2 = Trim<' str '> // 'str'
type t3 = TrimRight<'str '> // 'str'
实现方式
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 t1 = Capitalize<'hello'> // 'Hello'
type t2 = UnCapitalize<'Hello'> // 'hello'
实现方式
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
是用来向一个函数追加一个参数的,其用法如下:
// 结果:(a: number, b: number) => number
type result = AppendArgument<(a: number) => number, number>
实现方式
type AppendArgument<Fn, A> = Fn extends (...args: infer R) => infer T ? (...args: [...R, A]) => T : never
代码详解:
- 我们首先利用
infer
关键词得到了Fn
函数的参数类型以及返回类型,然后把新的参数添加到参数列表,并原样返回其函数类型。
Permutation(排列组合)
用法
Permutation
是用来将联合类型中的每一个类型进行排列组合,其用法如下:
// 结果1:['A', 'B'] | ['B', 'A']
type result1 = Permutation<'A' | 'B'>
// 结果2:['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']
type result2 = Permutation<'A' | 'B' | 'C'>
实现方式
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(数组降维)
用法
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
),其用法如下:
// 结果:{ id: number; name: string; }
type result = AppendToObject<{ id: number; }, 'name', string>
实现方式
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(字符串转数组)
TIP
此题不属于type-challenges类型挑战题
用法
StringToArray
是用来将一个字符串转换成一个数组的,其用法如下:
// 结果:['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(字符串转联合类型)
用法
在实现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 Foo = {
a: number;
b: string;
}
type Bar = {
b: number;
c: boolean;
}
// 结果:{ a: number; b: number; c: boolean; }
type result = Merge<Foo, Bar>
实现方式
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 Foo = {
id: number;
name: string;
age: string;
}
type Bar = {
name: string;
age: string;
gender: number;
}
// 结果:{ id: number; gender: number; }
type result = Diff<Foo, Bar>
实现方式
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(数组元素真值判断)
用法
AnyOf
用来判断数组元素真假值的,如果任一值为真,返回true
;数组为空或者全部为false
,才返回false
,其用法如下:
// 结果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>
实现方式
// way1: 排除法
type IsUnion<T, U = T> =
[T] extends [never]
? false
: T extends U
? [Exclude<U, T>] extends [never]
? false
: true
: false
// way2: 正反对比法
type IsUnion<T, U = T> =
(T extends U
? U extends T
? true
: unknown
: false
) extends true ? false : true
代码详解:上面的实现虽然代码不多,但可能无法一下子就弄明白,为了更好的理解这种实现方式,我们来看如下两个案例分析:
// 案例一
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 result1 = PercentageParser<'+85%'> // ['+', '85', '%']
type result2 = PercentageParser<'-85%'> // ['-', '85', '%']
type result3 = PercentageParser<'85'> // ['', '85', '']
实现方式
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(移除字符)
用法
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
是用来实现数字减一的,其用法如下:
用法
// 结果:99
type result = MinusOne<100>
实现方式
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(根据类型选取)
用法
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
。
用法
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 Person = {
readonly name: string;
age: number;
}
// 结果:{ name: string; age: number; }
type MutableResult = MyMutable<Person>
实现方式
type MyMutable<T> = {
-readonly [P in keyof T]: T[P]
}
代码解读:
-readonly
:表示把readonly
关键词去掉,去掉之后此字段变为可改的。
OmitByType(按类型移除)
OmitByType
的实现思路和PickByType
类似。
用法
OmitByType
是用来按照类型移除的,其用法如下:
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(数组按深度降维)
用法
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>
实现方式
// 一个二叉树节点
interface TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
}
// 先序遍历实现
type PreOrderTraversal<
T extends TreeNode | null
> = [T] extends [TreeNode]
? [T['val'], ...PreOrderTraversal<T['left']>, ...PreOrderTraversal<T['right']>]
: []
// 中序遍历实现
type InOrderTraversal<
T extends TreeNode | null
> = [T] extends [TreeNode]
? [...InOrderTraversal<T['left']>, T['val'], ...InOrderTraversal<T['right']>]
: []
// 后序遍历实现
type PostOrderTraversal<
T extends TreeNode | null
> = [T] extends [TreeNode]
? [...PostOrderTraversal<T['left']>, ...PostOrderTraversal<T['right']>, T['val']]
: []
代码详解:
[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 result = Fibonacci<5>
实现方式
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(大于)
用法
GreaterThan<T, N>
是来用判断正整数T是否大于正整数N的,其用法如下:
// 结果: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
是用来判断是否为一个元组的,用法如下:
// 结果:true
type result = IsTuple<[number]>
实现方式
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(移除)
用法
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
方法的,其用法如下:
// 结果:2
type result = IndexOf<[1, 2, 3, 4], 3>
实现方式
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(数组去重)
用法
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(限定范围数字)
用法
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
是用来检查字符串中是否存在重复字符的,其用法如下:
// 结果1:false
type result1 = CheckRepeatedChars<'abc'>
// 结果2:true
type result2 = CheckRepeatedChars<'abb'>
实现方式
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
是用来取数组中位数的,其用法如下:
// 结果1: [2]
type result1 = GetMiddleElement<[1, 2, 3]>
// 结果2: [2, 3]
type result2 = GetMiddleElement<[1, 2, 3, 4]>
实现方式
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
是用来实现计算数组中元素出现次数的,其用法如下:
// 结果1: { 1: 1, 2: 1, 3: 1 }
type result1 = CountArrayElement<[1, 2, 3]>
// 结果2: { 1: 2, 2: 2, 3: 1 }
type result2 = CountArrayElement<[1, 2, 2, 1, 3]>
实现方式
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>
实现方式
根据JavaScript
中BigInt的用法,其不能包含小数。所以实现方式如下:
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
是用来返回字符串中所有匹配索引的,其用法如下:
// 结果1:[11]
type result1 = FindAllIndex<'TypeScript type challenges', 'type'>
// 结果2:[2, 13]
type result2 = FindAllIndex<'TypeScript type challenges', 'pe'>
// 结果3:[]
type result3 = FindAllIndex<'TypeScript type challenges', ''>
实现方式
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(替换元组中第一个匹配项)
用法
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
中的柯里化的,其用法如下:
const func = Currying((a: number, b: string, c: boolean) => true)
// 结果:(a: string) => (a: number) => (a: boolean) => true
type funcType = typeof func
实现方式
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
所做的事情和&
符号是一样的,其用法如下:
// 结果:never
type result1 = UnionToIntersection<1 | 'foo' | true>
// 结果:{ a: number; b: number; c: boolean; }
type result2 = UnionToIntersection<{ a: number; b: number; } | { b: string | number; c: boolean; }>
// 结果:(a: boolean | number) => string
type result3 = UnionToIntersection<((a: boolean) => string | number) | ((a: number) => string)>
实现方式
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>
实现方式
按照RequiredKeys
的实现思路,能够很容易的实现GetRequired
。
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'>
实现方式
type IsLetter<S extends string> = Uppercase<S> extends Lowercase<S> ? false : true
type CamelCase<
S extends string,
R extends string = ''
> = S extends `${infer First}${infer Rest}`
? CamelCase<
Rest,
IsLetter<First> extends true
? R extends `${infer P}_`
? `${P}${Uppercase<First>}`
: `${R}${Lowercase<First>}`
: `${R}${First}`
>
: R
代码详解:
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'
}
// 结果:['string', 'dec']
type result = ParsePrintFormat<'Hello %s: score is %d'>
实现方式
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>
: []
代码详解:在以上实现方法中,借用了辅助数组的思想,拿上面案例来说,具体迭代分析如下:
// 第一次迭代
S满足条件 R = [] S1 = 'Hello ' Char = 's' S2 = ': score is %d'
// 第二次迭代
S满足条件 R = ['string'] S1 = ': score is ' Char = 'd' S2 = ''
// 最后一次迭代
S不满足条件 R = ['string', 'dec']
// 结果
result = R = ['string', 'dec']
VueBasicProps(Vue的Props类型)
用法
实现方式
IsAny和NotAny
用法
IsAny
是用来判断一个类型是否为any
的,NotAny
和它做的事情相反。
type t1 = IsAny<undefined> // false
type t2 = IsAny<never> // false
type t3 = IsAny<any> // true
type t4 = NotAny<undefined> // true
type t5 = NotAny<never> // true
type t6 = NotAny<any> // false
实现方式
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]
代码详解:对于Get
的实现,主要分为两部分:含有.
符号的字符串和不含.
符号的字符串。
- 含有
.
符号的字符串:对于这种情况,我们先判断.
符号左侧部分是否满足为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
工具,其实现代码如下:
type Multiply10<T extends any[]> = [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T]
type result = Multiply10<[1]> // [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
- 第四步:根据前几步的分析,把所有东西串联起来,
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
是用来从数组中移除指定元素的,其用法如下:
// 结果:[2]
type result = FilterOut<[1, 'a', 2], 'a' | 1>
实现方式
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
对于递归的次数存在限制,会提示嵌套过深。
用法
// 结果:91
type result = LengthOfString<'1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901'>
实现方式
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]
代码详解:
UnionToIntersection
: 联合类型取交集,在之前已经实现过,这里不再赘述。主要理解以下案例:
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)
用法
DeepPick
是用来深层次获取属性值的,用法如下:
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 Person = {
some_PROP: string;
prop: {
another_prop: string;
};
array: [
{ snake_case: string; }
]
}
type Expected = {
someProp: string;
prop: {
anotherProp: string;
};
array: [
{ snakeCase: string; }
]
}
// 结果:Expected
type result = Camelize<Person>
实现方式
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(移除全部字符)
用法
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?', ' '>
实现方式
type Split<
S extends string,
SEP extends string,
R extends any[] = []
> = S extends `${infer _}`
? S extends `${infer S1}${SEP}${infer S2}`
? Split<S2, SEP, [...R, S1]>
: S extends ''
? SEP extends ''
? R
: [...R, S]
: [...R, S]
: string[]
ClassPublicKeys(类的公共键)
用法
实现方式
IsRequiredKeys(是否为必填key)
用法
IsRequiredKeys
是用来判断是否为必填key
的,其用法如下:
type Obj = {
a: number,
b?: string
}
// 结果1:true
type result1 = IsRequiredKeys<Obj, 'a'>
// 结果2:false
type result2 = IsRequiredKeys<Obj, 'b'>
实现方式
type IsRequiredKey<T, K extends keyof T> = T extends Record<K, T[K]> ? true : false
根据IsRequiredKey
的实现思路,我们可以很容易实现IsOptionalKey
,如下:
type IsOptionalKey<T, K extends keyof T> = {} extends { [P in K]: T[P] } ? true : false
// 结果1:false
type result1 = IsOptionalKey<Obj, 'a'>
// 结果2:true
type result2 = IsOptionalKey<Obj, 'b'>
ObjectEntries(对象Object.entries方法)
用法
实现方式
IsPalindrome(是否为回文)
用法
实现方式
MutableKeys(所有可写键)
用法
实现方式
Intersection(交集)
用法
实现方式
BinaryToDecimal(二进制转十进制)
用法
实现方式
ObjectKeyPaths(对象属性键路径)
用法
实现方式
TwoSum(LeetCode两数之和)
用法
实现方式
ValidDate(校验是否为合法日期)
用法
实现方式
Assign(对象Object.assign方法)
用法
实现方式
Maximum(数字中的最大值)
用法
实现方式
DeepCapitalize(深度首字母大写)
用法
实现方式
UnionReplace(联合类型替换)
用法
实现方式
FizzBuzz(Fizz和Buzz输出问题)
用法
实现方式
RLE(运行长度编码)
用法
实现方式
ObjectPathArray(对象键路径数组)
用法
实现方式
SnakeCase(字符串下划线连接)
用法
实现方式
IsNegativeNumber(是否为负数)
用法
实现方式
OptionalUndefined(按需转换为可选属性)
用法
实现方式
地狱
撰写中...