TypeScript类型挑战:学习高级用法
TypeScript
工具和版本
版本和工具
- typescript: 4.3.2
- ts-node: 10.0.0
# 安装typescript
$ npm install -g typescript@4.3.2
# 安装ts-node
$ npm install -g ts-node@10.0.0
知识点
联合类型
联合类型表示多种取值中的一种,采用|
来进行分隔:
let value: string | number
value = 123 // 编译正确
value = '456' // 编译正确
value = true // 编译报错
- 当不确定为哪种联合类型的时候,只能取其公共属性或者方法。
// 会编译报错:不确定到底是string类型还是number类型
function getLength (value: string | number): number {
return value.length
}
// 不会编译报错:string和number都有toString()方法
function getString (value: string | number): string {
return value.toString()
}
- 当变量确定了其类型,那么
TS
会自动推导其对应的属性或方法。
let value: string | number
value = '123'
console.log(value.length) // 编译正确
value = 123
console.log(value.length) // 编译报错
字符串字面量
字符串字面量与联合类型的表现形式非常类似,它表示变量只能取某几个字符中的一种。
type EventName = 'click' | 'scroll' | 'mousemove'
function handleEvent (event: EventName) {
console.log(event)
}
handleEvent('click') // 编译正确
handleEvent('scroll') // 编译正确
handleEvent('dbclick') // 编译错误
元组
一个确定长度且每个位置元素类型也是确定的,这样的数组就可以叫元组。
根据以上定义,我们总结元组如下两个特点:
- 数组长度不匹配,会报错。
let arr: [string, number]
arr = ['123', 456, 789] // 编译报错
arr = ['123'] // 编译报错
arr = ['123', 456] // 编译正确
- 元素类型不匹配,会报错。
let arr: [string, number]
arr = [123, '456'] // 编译报错
arr = ['123', 456] // 编译正确
枚举
枚举类型用来表示取值限定在指定的范围,例如一周只能有七天,颜色只能有红、绿、蓝等。
enum Colors {
Red = 1,
Yellow = 2,
Blue = 3
}
// 正向取值
console.log(Colors.Red) // 1
console.log(Colors.Yellow) // 2
console.log(Colors.Blue) // 3
// 反向取值
console.log(Colors[1]) // Red
console.log(Colors[2]) // Yellow
console.log(Colors[3]) // Blue
函数重载
JavaScript
因为其动态语言的特性,并没有真正函数重载的概念,TypeScript
中的函数重载,只是对于同一个函数的多种声明(参数数量不同或参数类型不同),当函数开始进行匹配时,会优先从第一个函数声明开始匹配。
function getArea (width: number): number
function getArea (width: number, height?: number): number
function getArea (width: number, height?: number): number {
if (height) {
return width * height
} else {
return width * width
}
}
console.log(getArea(10, 20)) // 200
console.log(getArea(10)) // 100
类型守卫(类型断言)
当一个变量为联合类型的时候,TypeScript
需要进一步的判断才能推导出它到底属于什么类型,最常见的断言方式有is
和in
这两种。
is
断言
function isString (str: any): str is string {
return typeof str === 'string'
}
function getLength (value: string | number): number {
if (isString(value)) {
return value.length
}
return value.toString().length
}
console.log(getLength('123')) // 3
console.log(getLength(123)) // 3
in
断言
class Person {
sayHi () {
console.log('Hello~')
}
}
class Animal {
bark () {
console.log('汪汪汪')
}
}
function sound (obj: Person | Animal): void {
if ('sayHi' in obj) {
obj.sayHi()
} else {
obj.bark()
}
}
sound(new Person()) // Hello~
sound(new Animal()) // 汪汪汪
泛型
从一个例子认识泛型。
function getValue (obj, key) {
return obj[key]
}
如果要给以上方法添加TypeScript
支持,那么我们需要处理以下三个问题:
obj
的类型问题。key
类型的问题。getValue
函数返回值类型的问题。
我们初步改造一下以上方法:
function getValue (obj: any, key: string): any {
return obj[key]
}
我们发现,在不使用泛型的情况下,以上方法对于类型支持程度十分有限。接下来,我们采用泛型继续改造以上方法。
function getValue<T> (obj: T, key: keyof T): T[keyof T] {
return obj[key]
}
在改造完毕后,我们发现其中引入了一个新的关键词keyof
。其中keyof T
表示T
类型的键组成的联合类型。
const obj = {
name: 'AAA',
age: 23
}
// 'name'|'age'
type keys = keyof typeof obj
TS
中的keyof T
相当于JavaScript
中的Object.keys()
。
在介绍完keyof
后,我们继续完善getValue
方法,如下:
function getValue<T, U extends keyof T> (obj: T, key: U): T[U] {
return obj[key]
}
const obj = {
name: 'AAA',
age: 23
}
console.log(getValue(obj, 'name')) // 编译成功
console.log(getValue(obj, 'age')) // 编译成功
console.log(getValue(obj, 'sex')) // 编译失败, 'sex'属性不存在
代码详解:
U extends keyof T
:这段代码表示第二个泛型的取值限定在T
类型的键名范围,对应到实际函数调用则表示:函数第二个参数的取值,只能是obj
对象中的某一个键名。T[U]
:此代码表示取T
类型实际键名的定义类型作为返回值,对应到实际函数返回值上则表示:当第二个参数传递name
时,getValue()
函数返回值为string
;当第二个参数传递age
时,getValue()
函数返回值为number
。- 类型推断:虽然我们给
getValue()
方法定义了两个泛型T
和U
,但是我们在真正函数调用的时候并没有写泛型,TS
会根据实际传参自动推导其泛型。
// 自动推导
getValue<{name: string;age: number},'name'>(obj, 'name')
getValue<{name: string;age: number},'age'>(obj, 'age')
extends
在介绍泛型这一小节,我们引出了一个extends
关键词,对于extends
关键词的用法,一般有两种:类型约束和条件类型。
类型约束
类型约束经常和泛型一起使用,拿泛型小节中的例子来说明:
// 类型约束
U extends keyof T
keyof T
是一个整体,它表示一个联合类型。U extends Union
这一整段表示U
的类型被收缩在一个联合类型的范围内。
这样做的实际表现为:第二个参数传递的字符串只能是T
键名中的一个,传递不存在的键名会报错。
条件类型
常见的条件类型表现形式如下:
T extends U ? 'Y' : 'N'
我们发现条件类型有点像JavaScript
中的三元表达式,事实上它们的工作原理是类似的,例如:
type res1 = true extends boolean ? true : false // true
type res2 = 'name' extends 'name'|'age' ? true : false // true
type res3 = [1, 2, 3] extends { length: number; } ? true : false // true
type res4 = [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 test = Extract<type1, type2>
// 推理步骤
'name'|'age' extends 'name'|'address'|'sex' ? 'name'|'age' : never
=> ('name' extends 'name'|'address'|'sex' ? 'name' : never) |
('age' extends 'name'|'address'|'sex' ? 'age' : never)
=> '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
),如下:
type result = 'name' | never => 'name'
infer关键词
介绍
infer
关键词的作用是延时推导,它会在类型未推导时进行占位,等到真正推导成功后,它能准确的返回正确的类型。
为了更好的理解infer
关键词的用法,我们使用ReturnType
和PromiseType
这两个例子来说明。
ReturnType
ReturnType
是一个用来获取函数返回类型的工具。
type ReturnType<T> = T extends (...args: any) => infer R ? R : never
type UserInfo = {
name: string;
age: number;
}
function login(username: string, password: string): UserInfo {
const userInfo = { name: 'admin', age: 99 }
return userInfo
}
// 结果:{ name: string; age: number; }
type result = MyReturnType<typeof login>
代码详解:
T extends (...args: any) => infer R
:如果不看infer R
,这段代码实际表示:T
是不是一个函数类型。(...args: any) => infer R
:这段代码实际表示:一个函数,我们把它的参数使用args
来表示,把它的返回类型用R
来进行占位。- 如果
T
满足是一个函数类型,那么我们返回其函数返回类型,也就是R
,如果不是一个函数类型,我们返回never
。
PromiseType
PromiseType
是一个用来获取Promise
包裹的类型的工具。
type MyPromiseType<T> = T extends Promise<infer R> ? R : never
// 结果:string[]
type result = MyPromiseType<Promise<string[]>>
代码详解:
T extends Promise<infer R>
:这段代码实际表示:T
是不是一个Promise
包裹类型。Promise<infer R>
:这段代码实际表示:Promise
里面实际包裹的类型,我们使用R
来占位,例如:
Promise<string> => R = string
Promise<number> => R = number
- 如果
T
满足是一个Promise
包裹类型,那么返回其包裹的类型R
,否则返回never
。
挑战题
上面的基础知识部分,我们主要参考其他TypeScript
文章或者官网知识来进行讲解。
在挑战题部分,我们更偏向于使用上面的知识来解决实际的问题,这部分主要参考Type-Challenges,我们将这些挑战题按类型进行划分,主要分为:
- 内置工具类
- 内置工具类扩展
- 数组类
- 字符串类
- 递归类
- 实际场景题类
其中对于每一类,都可能包含简单
、一般
以及困难
等三个难度。
内置工具类
内置工具类,是指那些由TypeScript
官方默认提供的工具函数,你可以在lib.es5.d.ts
中找到它们的原生实现。
Required(必填)和Partial(可填)
Required
是用来将所有字段变成必填的,而Partial
所做的事情和它相反,它是用来将所有字段变成可填的。
type MyPartial<T> = {
[P in keyof T]?: T[P]
}
type MyRequired<T> = {
[P in keyof T]-?: T[P]
}
type Person = {
name: string;
age?: number;
}
// 结果:{ name: string; age: number; }
type result1 = MyRequired<Person>
// 结果:{ name?: string; age?: number; }
type result2 = MyPartial<Person>
代码详解:
keyof T
:这段代码是获取T
类型中的所有键,所有键组合成一个联合类型,例如:'name'|'age'
。P in keyof T
:P in
属于一个迭代过程,可以用JavaScript
中的for in
迭代来理解,例如:
P in 'name'|'age'
// 第一次迭代:P = 'name'
// 第二次迭代:P = 'age'
T[P]
:属于一个正常的取值操作,在TypeScript
中,不能通过T.P
的形式取值,而应该用T[P]
。-?
:这段代码可以从语义上直观的理解成:去掉?
这个符号。
Readonly(只读)和Mutalbe(可写)
Readonly
是用来将所有字段变成只读的,Mutable
所做的事情跟它相反,它是用来将所有字段变成可写的。
type MyReadonly<T> = {
readonly [P in keyof T]: T[P]
}
type MyMutable<T> = {
-readonly [P in keyof T]: T[P]
}
type Person = {
name: string;
readonly age?: number;
}
// 结果:{ readonly name: string; readonly age?: number; }
type result1 = MyReadonly<Person>
// 结果:{ name: string; age?: number; }
type result2 = MyMutable<Person>
代码详解:
- 这里出现的
keyof
和in
,我们在上面的例子中已经详细进行了描述,这里不再赘述。 -readonly
:这段代码表示把readonly
关键词去掉,去掉之后就从只读变成了可写。
Record(构造)
Record
的作用有点像JavaScript
中的map
方法,它是用来将K
的每一个键(k
)指定为T
类型,这样由多个k/T
组合成了一个新的类型。
type MyRecord<k extends keyof any, T> = {
[P in K]: T
}
type pageList = 'login' | 'home'
type PageInfo = {
title: string;
url: string;
}
type Expected = {
login: { title: string; url: string; };
home: { title: string; url: string; };
}
// 结果:Expected
type result = MyRecord<pageList, PageInfo>
代码详解:
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
Pick(选取)
Pick
的作用是从指定的类型中选取指定的字段组成一个新的类型。
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
type Person = {
name: string;
age: number;
address: string;
}
// 结果:{ age: number; address: string; }
type result = MyPick<Person, 'age' | 'address'>
K extends keyof T
:表示K
只能是keyof T
的子类型,如果我们在使用Pick
的时候传递了不存在于T
的字段,会报错:
// 报错:phone无法分配给keyof T
type result = MyPick<Person, 'name' | 'phone'>
Exclude(排除)
Exclude
的作用是从T
中移除那些存在于U
中的类型,既:取差集。
type MyExclude<T, U> = T extends U ? never : T
// 结果:'name'
type result = MyExclude<'name'|'age'|'sex', 'age'|'sex'>
代码详解:我们之前在extends
章节分析过一个类似的问题,只不过之前分析的是求交集,这里是求差集。但原理是类似的,都是属于分布式条件类型的概念。
T extends U ? never ? T
// 第一次迭代分发:
'name' extends 'age' | 'sex' ? never : 'name' => 'name'
// 第二次迭代分发:
'age' extends 'age' | 'sex' ? never : 'age' => never
// 第三次迭代分发:
'sex' extends 'age' | 'sex' ? never : 'sex' => never
// 结果:
type result = 'name' | never | never => 'name'
Omit(剔除)
Omit
所做的事情和Pick
相反,Omit
指的是从指定的T
类型中移除指定的字段,剩下的字段组成一个新类型。
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
type MyExclude<T, U> = T extends U ? never : T
type MyOmit<T, K> = MyPick<T, MyExclude<keyof T, K>>
type Person = {
name: string;
age: number;
address: string;
}
// 结果:{ name: string;age:number; }
type result = MyOmit<Person, 'address'>
代码详解:
- 使用
MyExclude<keyof T, K>
,我们能从T
中移除指定的字段,得到一个联合类型,例如:'name'|'age'
- 使用
MyPick<T, 'name'|'age'>
,我们可以从T
中选取这两个字段组合成一个新的类型。
内置工具类扩展
内置工具类扩展这小节,我们结合之前的Required
和Readonly
这两个内置工具,扩展出RequiredKeys
、OptionalKeys
、GetRequired
和GetOptional
这几个工具方法。
RequiredKeys(所有必填字段)
RequiredKeys
是用来获取所有必填字段,其中这些必填字段组合成一个联合类型。
type RequiredKeys<T> = {
[P in keyof T]: T extends Record<P,T[P]> ? P : never
}[keyof T]
type Person = {
name: string;
age?: number;
address?: string;
}
// 结果:'name'
type result = RequiredKeys<Person>
RequiredKeys
实现思路:
- 第一步:将
key/value
构造成key/key
的形式:
type RequiredKeys<T> = {
[P in keyof T]: P
}
type Person = {
name: 'name';
age?: 'age';
address?: 'address';
}
- 第二步:
T[keyof T]
取值得到一个联合类型:
type RequiredKeys<T> = {
[P in keyof T]: P
}[keyof T]
type Person = {
name: 'name';
age?: 'age';
address?: 'address';
}
// 'name'|'age'|'address'
type keys = RequiredKeys<Person>
- 第三步:理解
TS
中的类型关系,类型具体的是子类,类型宽泛的是父类。
// 结果: true
type result1 = Person extends { name: string; } ? true : false
// 结果:false
type result2 = Person extends { age?: number; } ? true : false
依据以上的知识点,我们用如下一行代码来表示这种关系:
T extends Record<P, T[P]> ? P : never
再将以上例子带入,结果为:
Person extends Record<'name', string> ? 'name' : never // 'name'
Person extends Record<'age', number> ? 'age' : never // never
- 第四步:完整实现
type RequiredKeys<T> = {
[P in keyof T]: T extends Record<P,T[P]> ? P : never
}[keyof T]
OptionalKeys(所有可选字段)
type OptionalKeys<T> = {
[P in keyof T]: {} extends Pick<T, P> ? P : never
}[keyof T]
type Person = {
name: string;
age?: number;
address?: string;
}
// 结果:'age'|'address'
type result = OptionalKeys<Person>
实现思路:
- 第一步:将
key/value
构造成key/key
的形式:
type OptionalKeys<T> = {
[P in keyof T]: P
}
type Person = {
name: 'name';
age?: 'age';
address?: 'address';
}
- 第二步:
T[keyof T]
取值得到一个联合类型:
type OptionalKeys<T> = {
[P in keyof T]: P
}[keyof T]
type Person = {
name: 'name';
age?: 'age';
address?: 'address';
}
// 'name'|'age'|'address'
type keys = OptionalKeys<Person>
- 第三步:理解
TS
中的类型关系,类型具体的是子类,类型宽泛的是父类。
// 结果: true
type result = {} extends { age?: string; } ? true : false
你可能对{} extends { age?: string; }
这段代码为true
并不是很理解,那么我们换一种思路来理解:
type result = {} extends {} | {age: string;} ? true : false
根据以上案例,我们使用一行代码来表示这种关系:
{} extends Pick<T, P> ? P : never
带入我们的例子,结果如下:
{} extends Pick<Person, 'name'> ? 'name' : never => 'name'
{} extends Pick<Person, 'age'> ? 'age' : never => never
{} extends Pick<Person, 'address'> ? 'address' : never => never
- 完整实现
type OptionalKeys<T> = {
[P in keyof T]: {} extends Pick<T, P> ? P : never
}[keyof T]
GetRequired(所有必填类型)
GetRequired
是用来获取一个类型中,所有必填键及其类型所组成的一个新类型的。
在实现了RequiredKeys
的基础上,我们很容易实现GetRequired
type GetRequired<T> = {
[P in RequiredKeys<T>]-?: T[P]
}
type Person = {
name: string;
age?: number;
address?: string;
}
// 结果: { name: string; }
type result = GetRequired<Person>
GetOptional(所有可选类型)
GetOptional
是用来获取一个类型中,所有可选键及其类型所组成的一个新类型的。
在实现了OptionalKeys
的基础上,我们很容易实现GetOptional
type GetOptional<T> = {
[P in OptionalKeys<T>]?: T[P]
}
type Person = {
name: string;
age?: number;
address?: string;
}
// 结果: { age?: number; address?: string; }
type result = GetOptional<Person>
数组类
匹配数组时,经常会使用到infer
关键词,通常有如下几种常见的用法:
// 匹配空数组
T extends [] ? true : false
// 数组前匹配
T extends [infer L, ...infer R] ? L : never
// 数组后匹配
T extends [...infer L, infer R] ? R: never;
FirstOfArray(数组第一个元素)
利用数组前匹配的思想,可以很容易的实现获取数组第一个元素FirstOfArray
这个工具。
type FirstOfArray<T extends any[]> = T extends [infer L, ...infer R] ? L : never
// 结果:1
type result = FirstOfArray<[1, 2, 3]>
代码详解:
T extends any[]
:限定T
类型必须是一个数组类型。T extends [infer L, ...infer R]
:T
是不是一个以L
为第一个元素,其余元素以R
占位的数组形式,其中R
可以是空数组,下面这几种形式都满足上述条件。
// L = 1, R = [2, 3, 4]
const arr1 = [1, 2, 3, 4]
// L = 1, R = []
const arr2 = [1]
LastOfArray(数组最后一个元素)
利用数组后匹配的思想,可以很容易的实现获取数组最后一个元素LastOfArray
这个工具。
type LastOfArray<T extends any[]> = T extends [...infer L, infer R] ? R : never
// 结果:3
type result = Last<[1, 2, 3]>
代码详解:
T extends [...infer L, infer R]
:T
是不是一个以R
为最后一个元素,其余元素以L
占位的数组形式,其中L
可以是空数组,下面这几种形式都满足上述条件。
// L = [1, 2, 3], R = 4
const arr1 = [1, 2, 3, 4]
// L = [] R = 1
const arr2 = [1]
ArrayLength(数组长度)
要获取数组的长度,可以直接使用T['length']
的方式进行获取。
注意:这里不能使用T.length
的形式进行取值。
type ArrayLength<T extends readonly any[]> = T['length']
// 结果:3
type result = ArrayLength<[1, 2, 3]>
扩展:在上面的实现方案中,我们只能传递一个一般数组,如果想兼容传递类数组,则需要改动代码,如下:
type ArrayLength<T> = T extends { length: number } ? T['length'] : never
type result1 = ArrayLength<[1, 2, 3]> // 3
type result2 = ArrayLength<{ 0: '0', length: 12 }> // 12
Concat(数组concat方法)
根据数组concat
的用法,我们只需要把两个数组,使用展开运算符...
,展开到一个新数组中即可。
type MyConcat<T extends any[], U extends any[]> = [...T, ...U]
// 结果:[1, 2, 3, 4]
type result = MyConcat<[1, 2], [3, 4]>
代码详解:
T extends any[]
:限定T
必须是一个数组类型,U
也是如此。[...T, ...U]
:这个T
和U
代表都代表数组类型,因此可以使用展开运算符展开数组。
Includes(数组includes方法)
在TS
中,一个数组中的所有元素,可以使用T[number]
来表示,它是一个联合类型,由所有元素共同组成。
type MyIncludes<T extends any[], K> = K extends T[number] ? true : false
type result1 = MyIncludes<[1, 2, 3, 4], '4'> // false
type result2 = MyIncludes<[1, 2, 3, 4], 4> // true
代码详解:
T[number]
:表示由数组所有元素公共组成的一个联合类型,例如:1 | 2 | 3 | 4
。
Push和Pop(数组Push和Pop方法)
Push
方法很好实现,对于Pop
方法,我们需要使用数组后匹配的思想来做。
type MyPush<T extends any[], K> = [...T, K]
type MyPop<T extends any[]> = T extends [...infer L, infer R] ? L : never
type result1 = MyPush<[1, 2, 3], 4> // [1, 2, 3, 4]
type result2 = MyPush<[1, 2, 3], [4, 5]> // [1, 2, 3, [4, 5]]
type result3 = MyPop<[1, 2, 3]> // [1, 2]
type result4 = MyPop<[1]> // []
字符串类
匹配字符串与匹配数组,都会用到infer
关键词,但是它们的表现形式不太相同,例如:
S extends `${infer S1}${infer S2}` ? S1 : never
T extends [infer L, ...infer R] ? L : never
StringLength(字符串长度)
实现思路:运用递归、infer
占位和辅助数组的思想来实现。
type StringLength<S extends string, T extends any[] = []> =
S extends `${infer S1}${infer S2}`
? LengthOfString<S2, [...T, S1]>
: T['length']
type result1 = StringLength<''> // 0
type result2 = StringLength<'123'> // 3
type result3 = StringLength<' 1 2 3 '> // 7
我们以上面第二个例子为例,进行详细说明:
type result2 = StringLength<'123'> // 3
// 第一次递归调用,满足S extends `${infer S1}${infer S2}`
S = '123' S1 = '1' S2 = '23' T = []
// 第二次递归调用,满足S extends `${infer S1}${infer S2}`
S = '23' S1 = '2' S2 = '3' T = ['1']
// 第三次递归调用,满足S extends `${infer S1}${infer S2}`
S = '3' S1 = '3' S2 = '' T = ['1', '2']
// 第四次递归调用,不满足S extends `${infer S1}${infer S2}`
S = '' S1 = '' S2 = '' T = ['1', '2', '3']
// 结果:3
type result = T['length']
Capitalize(首字母大写)
Capitalize
是用来将字符串首字母大写,会使用到内置的Uppercase
这个工具。
type MyCapitalize<S extends string> =
S extends `${infer S1}${infer S2}`
? `${Uppercase<S1>}${S2}`
: S
// 结果:Abc
type result = MyCapitalize<'abc'>
代码详解:字符串'abc'
满足${infer S1}${infer S2}
这种格式,此时S1 = 'a'
,S2 = 'bc'
,再将S1
和S2
拼接起来,其中S1
使用Uppercase
内置工具转换成大写形式,变成A
,所以结果为:Abc
。
扩展:根据以上思路,我们可以写一个MyUnCapitalize
工具,它所做的事情和MyCapitalize
相反,实现代码如下:
type MyUnCapitalize<S extends string> =
S extends `${infer S1}${infer S2}`
? `${Lowercase<S1>}${S2}`
: S
StringToArray(转数组)
借用StringLength
的实现思路,我们很容易能实现StringToArray
这个工具。
type StringToArray<S extends string, T extends any[] = []> =
S extends `${infer S1}${infer S2}`
? StringToArray<S2, [...T, S1]>
: T
// 结果:['a', 'b', 'c']
type result = StringToArray<'abc'>
StringToUnion(转联合)
运用递归的思想,我们也能很快的实现StringToUnion
这个工具,它的作用是将字符串转成一个联合类型。
type StringToUnion<S extends string> =
S extends `${infer S1}${infer S2}`
? S1 | StringToUnion<S2>
: never
// 结果:'a'|'b'|'c'
type result = StringToUnion<'abc'>
CamelCase(连字符转小驼峰)
CamelCase
是用来将连字符的字符串,转换成小驼峰的形式的工具。
type CamelCase<S extends string> =
S extends `${infer S1}-${infer S2}`
? S2 extends Capitalize<S2>
? `${S1}-${CamelCase<S2>}`
: `${S1}${CamelCase<Capitalize<S2>>}`
: S
// 结果:'fooBarBaz'
type result = CamelCase<'foo-bar-baz'>
代码详解:CamelCase
的实现,同样使用到了递归的思路,我们以上面例子为例进行详细的说明:
type result = CamelCase<'foo-bar-baz'>
// 第一次递归调用 S满足${infer S1}-${infer S2} S2不满足extends Capitalize<S2>
S = 'foo-bar-baz' S1 = 'foo' S2 = 'bar-baz'
// 第二次递归调用 S满足${infer S1}-${infer S2} S2不满足extends Capitalize<S2>
S = 'Bar-baz' S1 = 'Bar' S2 = 'baz'
// 第三次递归调用 S不满足${infer S1}-${infer S2}
S = 'Baz'
// 结果:fooBarBaz
type result = 'foo' + 'Bar' + 'Baz' => 'fooBarBaz'
Get(属性路径取值)
你可能听说过,或者使用过lodash
工具库的get
方法,它支持对一个对象进行字符串路径的形式进行取值,例如:
import _ from 'lodash'
const obj = {
foo: {
bar: {
value: 'foobar',
count: 6,
},
included: true,
},
hello: 'world'
}
console.log(_.get(obj, 'foo.bar.value')) // 'foobar'
我们同样可以在TS
中,实现此功能,既Get
工具。
type Get<T, K extends string> =
K extends `${infer S1}.${infer S2}`
? S1 extends keyof T
? Get<T[S1], S2>
: never
: K extends keyof T
? T[K]
: never
type Data = {
foo: {
bar: {
value: 'foobar',
count: 6,
},
included: true,
},
hello: 'world'
}
type result1 = Get<Data, 'hello'> // 'world'
type result2 = Get<Data, 'foo.bar.value'> // 'foobar'
type result3 = Get<Data, 'baz'> // never
代码详解:
- 对第一个例子而言,它不满足
${infer S1}.${infer S2}
的形式,但是满足keyof T
,此时我们只需要简单的根据键名取值即可,因此结果为'world'
- 对第二个例子而言,它满足
${infer S1}.${infer S2}
的形式,此时S1='foo'
,S1
又满足keyof T
的形式,所以存在递归调用,我们以下面代码来详细拆解递归的过程。
// 第一次递归调用,K满足`${infer S1}.${infer S2},S1满足keyof T
T = Data K = 'foo.bar.value' S1 = 'foo' S2 = 'bar.value'
// 第二次递归调用,K满足`${infer S1}.${infer S2},S1满足keyof T
T = Data['foo'] K = 'bar.value' S1 = 'bar' S2 = 'value'
// 第三次递归调用,K不满足`${infer S1}.${infer S2},K满足keyof T
T = Data['foo']['bar'] K = 'value'
// 结果: 'foobar'
type result = Data['foo']['bar']['value']
StringToNumber(转数字)
StringToNumber
是用来将字符串类型的数字,转换成真正数字类型数字的工具。
在JavaScript
中,我们可以很方便的调用Number()
方法或者parseInt()
方法来将字符串类型的数字,转换成数字类型的数字。但在TS
中,并没有这样的方法,需要我们来手动实现。
// 结果:123
type result = StringToNumber<'123'>
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
根据以上思路,我们还需要一个乘十的工具函数,对应到实际需求,就是需要把一个数组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
递归类
尽管在之前的挑战题中,我们已经用到了递归,但依然需要特地介绍一下。我们选取具有代表性的DeepReadonly
和DeepPick
来进行说明。
DeepReadonly(深度Readonly)
DeepReadonly
是用来将一个类型中所有字段都变成只读的一个工具,目前我们只需要考虑嵌套对象。
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends { [key: string]: any }
? DeepReadonly<T[P]>
: T[P]
}
type Person = {
name: string;
age: number;
job: {
name: string;
salary: number;
}
}
type Expected = {
readonly name: string;
readonly age: number;
readonly job: {
readonly name: string;
readonly salary: number;
}
}
// 结果:Expected
type result = DeepReadonly<Person>
代码详解:在DeepReadonly
的实现中,我们使用T[P] extends { [key: string]: any }
来判断当前值是不是一个对象,是使用索引签名来匹配的。如果满足是一个对象,那么再递归的调用DeepReadonly
。
// 第一次遍历,P = 'name' T[P]不是一个对象
// 第二次遍历,P = 'age' T[P]不是一个对象
// 第三次遍历,P = 'job' T[P]是一个对象
// 递归第一次遍历,P = 'name' T[P]不是一个对象
// 递归第二次遍历,P = 'salary' T[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,
}
},
obj3: {
j: number,
k: string,
l: boolean,
}
}
type Expected = {
obj: {
obj2: {
g: number
}
}
}
// 结果:Expected
type result = DeepPick<Obj, 'obj.obj2.g'>
你可能会对这种取值方式十分熟悉,没错,它的实现思路我们之前实现的Get
属性路径取值十分相似,不过需要做一点小小的改动,完整代码如下:
// 和DeepPick做对比
type Get<T, K extends string> =
K extends `${infer S1}.${infer S2}`
? S1 extends keyof T
? Get<T[S1], S2>
: never
: K extends keyof T
? T[K]
: never
type DeepPick<T, S extends string> =
S extends `${infer S1}.${infer S2}`
? S1 extends keyof T
? { [K in S1]: Get<T[S1], S2> }
: never
: S extends keyof T
? { [K in S]: T[K] }
: never
场景题
Join方法
Join
方法有点类似数组的join
方法,但又有点不一样,具体测试案例如下:
// 案例一:无参数,返回空字符串
join('-')() // ''
// 案例二:只有一个参数,直接返回
join('-')('a') // 'a'
// 案例三:有多个参数,以分隔符分割
join('')('a', 'b') // 'ab'
join('#')('a', 'b') // 'a#b'
我们需要实现一个这样的Join
方法,它的原型如下:
declare function join(delimiter: any): (...parts: any[]) => any
实现思路:
- 第一步:为了能够获取到
delimiter
和parts
,我们引入两个泛型。
declare function join<D extends string>(delimiter: D): <P extends string[]>(...parts: P) => any
- 第二步:为了能够处理
Join
函数的返回值,我们定义一个JoinType
来表示。
type JoinType<D extends string, P extends string[]> = any
declare function join<D extends string>(delimiter: D): <P extends string[]>(...parts: P) => JoinType<D, P>
- 第三步:根据之前撰写的案例,完善
JoinType
工具函数。
type Shift<T extends any[]> = T extends [infer L, ...infer R] ? R : []
type JoinType<D extends string, P extends string[]> =
P extends []
? ''
: P extends [infer Head]
? Head
: `${P[0]}${D}${JoinType<D, Shift<P>>}`
JoinType
实现详解:
Shift
:需要实现一个类似数组的shift
的方法,它的作用是从数组头部移除元素,使用的是数组前匹配的思想。P extends []
:这里判断P
是否为一个空数组,覆盖案例一。P extends [infer Head]
:这里判断p
是否是只有一个元素的数组,覆盖案例二。${P[0]}${D}${JoinType<D, Shift<P>>}
:这里运用到了递归的思想,主要流程就是把P
中的元素一个一个使用分隔符D
链接起来。
// 案例三: D = '#', P = ['a', 'b']
// 第一次迭代,P不为空数组且是多元素数组。
P = ['a', 'b'] result = `a#`
// 第二次迭代,P不为空数组,但是只有一个元素。
P = ['b'] result = `a#b`
// 结果
type result = Join('#')('a', 'b') // 'a#b'
Chainable链式调用
在日常的开发过程中,会经常使用到链式调用。现有一个对象,它有options
和get
这两个方法,其中option
方法用来添加新属性,get
方法用来获取最新的对象,使用案例如下:
// 结果:{ foo: 123, bar: { value: 'Hello' }, name: 'TypeScript' }
const result = obj
.option('foo', 123)
.option('bar', { value: 'Hello' })
.option('name', 'TypeScript')
.get()
现要实现一个Chainable
工具函数,来支持对象的这种行为,实现思路如下:
- 第一步:
Chainable
首先要满足对象中存在option
和get
这两个方法。
type Chainable<T> = {
option(key: any, value:any): any
get(): any
}
- 第二步:处理
get
函数的返回类型
type Chainable<T> = {
option(key: any, value:any): any
get(): T
}
- 第三步:处理
option
函数的参数类型及其返回类型。
type Chainable<T = {}> = {
options<K extends string, V>(key: K, value:V): Chainable<T & { [P in K]: V }>
get(): { [P in keyof T]: T[P] }
}
在这一步中,出现了一个新的知识点:交叉类型,可以使用JavaScript
中的对象Merge
来理解。
注意:当两个联合类型使用&
进行交叉式,其表现方式有点不用,它是取交集。
// 结果:{ name: string; age: number; address: string; }
type result1 = { name: string; age: number; } & { address: string; }
// 结果:'age'
type result2 = ('name' | 'age') & ('age' | 'address')
- 第四步:测试。
type Expected = {
foo: number
bar: {
value: string
}
name: string
}
declare const obj: Chainable<Expected>
// test
const result = obj
.options('foo', 123)
.options('bar', { value: 'Hello' })
.options('name', 'TypeScript')
.get()
参考
转载自:https://juejin.cn/post/6980158674681462820