Typescript 类型编程

泛型编程

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性,主要作用是对不特定数据类型的支持,以实现函数、接口(Interface)、类的复用。

泛型类似于函数,函数的本质是推后执行(调用),部分待定(函数参数)的代码,泛型的本质是推后执行(调用),部分待定(泛型参数)的类型。

1
2
type F<A, B> = A | B
type Result = F<string, number> // 调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface List<A> {
[index: number]: A
}
type X = List<string>

// 使用代入法查看泛型调用后的具体类型
type X = {
[index: number]: string
}

// 默认类型
interface Hash<V = string> {
[key: string]: V
}
  • 泛型中使用 extends 和三元运算符

泛型中使用 extends,判断是否为子类型。

1
2
3
4
5
6
7
8
9
10
11
type Person = {
id: number;
name: string;
age?: number;
}

type LikeString<T> = T extends string ? true : false // 如果 T 继承/包含于 string(T 是 string 的子类型),那就返回 true
type LikeNumber<T> = T extends number ? 1 : 2
type LikePerson<T> = T extends Person ? 'yes' : 'no'

type Result = LikeString<'a'>

T 为联合类型,则会使用泛型分配律,分开计算。

1
2
3
4
5
type ToArray<T> = T extends unknown ? T[] : never
type Result = ToArray<string | number> // 结果是 string[] | number[],而不是 (string | number)[]

// 代入法计算过程
type Result = (string extends unknown ? string[] : never) | (number extends unknown ? number[] : never)

Tnever,则直接返回 never,不会进入条件运算。

1
2
type Result = LikeString<never> // never
type Result = ToArray<never> // never

上述规则只对泛型有效,不在泛型中的联合类型和 never,按正常规则计算。

1
type Result = never extends unknown ? 1 : 2 // never 空集属于 unknown 的子集,所以返回 1,不是 never
  • 泛型中使用 keyof
1
2
type GetKeys<T> = keyof T // keyof 返回的是联合类型
type Result = GetKeys<Person> // id | age | name,注,无法直接调试 Result 结果,只能通过声明变量观察结果 const r: Result = 'id'

在泛型中使用泛型约束 extends keyof

1
2
3
4
// 获取 key 的类型
// 约束 K 的类型,只能从 keyof T 中取,不可以随意传
type GetKeyType<T, K extends keyof T> = T[K] // type X = Person['age']
type Result = GetKeyType<Person, 'age'> // number
  • 映射类型

映射类型(Mapped Types)将现有类型根据某种映射规则转换为一种新的类型。映射类型常通过 keyofin、修饰符(?readonly-readonly-?)和条件类型(Conditional Types)实现。以几种内置的工具类型 PartialRequiredRecordExcludeExtractPickOmit 为例,看其如何使用映射类型的。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Readonly 只读属性
type X1 = Readonly<Person>
// Readonly 实现如下
type Readonly<T> = {
// [K in keyof T] 表示对类型 T 的每个属性进行映射,并使用 readonly 关键字将其设为只读
// 不能写成索引签名 [K: keyof T]: T[K]
readonly [K in keyof T]: T[K]
}

// 删除 readonly,可写属性
type Mutable<T> = {
-readonly [K in keyof T]: T[K]
}
1
2
3
4
5
6
// Partial 给所有属性加上可选
type Result = Partial<Person>
// Partial 实现如下
type Partial<T> = {
[K in keyof T]?: T[K]
}
1
2
3
4
5
6
// Required 必选属性
type Result = Required<Person>
// Required 实现如下
type Required<T> = {
[K in keyof T]-?: T[K]
}
1
2
3
4
5
6
7
// Record 用于定义对象类型
type Result = Record<string, string>
// Record 实现如下
// 对象的 Key,只能有 3 种类型,需要约束
type Record<Key extends string | number | symbol, Val> = {
[K in Key]: Val
}
1
2
3
4
5
6
// Exclude 从联合类型中排除指定类型(求差集)
type Result = Exclude<1 | 2 | 3, 1 | 2> // 3
// Exclude 实现如下
// 联合类型会分开计算
// 1 | 2 | 3 extends 1 | 2 会分开为 type Result = (1 extends 1 | 2 ? never : 1) | (2 extends 1 | 2 ? never : 2) | (3 extends 1 | 2 ? never : 3)
type Exclude<A, B> = A extends B ? never : A
1
2
3
4
// Extract 求交集
type Result = Extract<1 | 2 | 3, 2 | 4> // 2
// Extract 实现如下,为 Exclude 的反向操作
type Extract<A, B> = A extends B ? A : never
1
2
3
4
5
6
// Pick 选择指定属性
type Result = Pick<Person, 'name' | 'age'>
// Pick 实现如下
type Pick<T, Key extends keyof T> = {
[K in Key]: T[K]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// Omit 忽略指定属性
type Result = Omit<Person, 'name' | 'age'>
// Omit 实现如下
// 核心是 as 后的 (K extends Key ? never : K),当前 K 在 Key 中,则删除,否则保留
type Omit<T, Key> = {
// [K in keyof T as never]: T // 断言为 never,会删除所有
// [K in keyof T as 'id']: T // 断言为 id,只会保留 id
[K in keyof T as (K extends Key ? never : K)]: T
}

// TS 原码中通过 Pick 来实现 Omit(反向思路)
// Pick<T, 不要的 Key>
type Omit<T, Key extends keyof T> = Pick<T, Exclude<keyof T, Key>>

类型体操

JavaScript 中可以对进行各种运算(算术运算、逻辑运算、比较运算…)以及循环、判断流的程控制和函数、面向对象等高级特性,如果把 TypeScript 的类型系统当作一门语言,TypeScript 可以对类型进行各种运算以及循环、判断流程控制和泛型等高级特性。

以计算斐波那契数列第 n 项值为例,使用 TypeScript 类型实现和 JavaScript 解释为以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Fibonacci<
T extends number,
// 循环下标(数组 length 表示)
TArray extends ReadonlyArray<unknown> = [unknown, unknown, unknown],
// 前前一项值(数组 length 表示)
PrePre extends ReadonlyArray<unknown> = [unknown],
// 前一项值(数组 length 表示)
Pre extends ReadonlyArray<unknown> = [unknown],
> = T extends 1
? 1
: T extends 2
? 1
: TArray['length'] extends T // 如果已经循环了 T 次
? [...Pre, ...PrePre]['length'] // 前两项相加
: Fibonacci<T, [...TArray, unknown], Pre, [...Pre, ...PrePre]>

type Result = Fibonacci<6> // 8,(1 + 1 + 2 + 3 + 5 + 8)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function fibonacci(n) {
if (n === 1 || n === 2) {
return 1
} else {
let prePre = 1
let pre = 1
let result = 0

for (let i = 3; i <= n; i++) {
result = pre + prePre
prePre = pre
pre = result
}

return result
}
}

fibonacci(6) // 结果是 8 (1 + 1 + 2 + 3 + 5 + 8)

注:TypeScript 类型体操是纯粹的函数式编程,函数式编程最基本原则是数据不可变,TypeScript 中类型一但声明就不能改变。

体操的基本原理

TypeScript 类型体操的本质是可以对类型进行循环、判断、交叉、联合、泛型等运算和操作。

  • 循环

TypeScript 中除了可以通过递归实现循环外,还可以通过分布式条件类型或映射类型实现,但它们都不能传递类型。

1
2
3
4
// 分布式条件类型
// 当泛型参数 T 为联合类型时,条件类型即为分布式条件类型,会将 T 中的每一项分别分发给 extends 进行比对
type Example<T> = T extends number ? T : never
type Result = Example<'1' | '2' | 3 | 4> // 3 | 4
1
2
3
4
5
// 映射类型
type Example<T extends string | number> = {
[Key in T]: Key
}
type Result = Example<'1' | '2' | 3 | 4> // { 1: '1'; 2: '2'; 3: 3; 4: 4; }
  • 判断

使用三名运算实现条件判断。

1
2
3
4
// if (A <= B) true else false
type A = 1
type B = 1 | 2
type Result = A extends B ? true : false // A 包含于 B,返回 true 否则 false
1
2
3
4
5
6
7
8
9
10
11
12
// if (A <= B) and (C <= D) ...
type A = 1
type B = 1 | 2
type C = 3
type D = 3 | 4
type Result = A extends B
? C extends D
? 'true, true'
: 'true, false'
: C extends D
? 'false, true'
: 'false, false'
1
2
3
4
5
// 判断空元组
type A = []
// 约束 Arr 必须是一个数组
type IsEmptyArray<Arr extends unknown[]> = Arr['length'] extends 0 ? true : false
type Result = IsEmptyArray<A>
1
2
3
4
5
6
7
8
9
// 判断非空元组
type A = []
type IsNotEmptyArray<Arr extends unknown[]> = Arr['length'] extends 0 ? false : true
type Result = IsNotEmptyArray<A>

// 或者这样写
// [...infer X, infer Y] 表示至少有一个,也可写为 [...unknown[], unknown],X、Y 相当于类型变量,加 infer 表示引用类型变量,这样就可不用写实际类型
// ...相当于 JS 中的 rest 运算符,...unknown[] 表示 0 个或无数个
type IsNotEmptyArray<Arr extends unknown[]> = Arr extends [...infer X, infer Y] ? true : false
  • 递归
1
2
3
4
5
6
// 反转元组
type A = ['ji', 'ni', 'tai', 'mei']
type Reverse<Arr extends unknown[]> = Arr extends [...infer Rest, infer Last]
? [Last, ...Reverse<Rest>]
: Arr
type Result = Reverse<A> // ['mei', 'tai', 'ni', 'ji']

注:经测试,元组递归的层数最多 48 层,普通对象的层数限制未测出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 未知层数的对象类型属性添加 readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>
}
interface SomeObject {
a: {
b: {
c: number
}
}
}

const obj: DeepReadonly<SomeObject> = {a: {b: {c: 2}}}
obj.a.b.c = 23 // 报错
  • 比较运算

TypeScript 中没有 ==,但是通过 extends 可以实现类似的效果。

1
2
3
4
5
6
7
8
type EqEq<T1, T2> = [T1] extends [T2] 
? ([T2] extends [T1] ? true : false)
: false;

type Result1 = EqEq<5, 10>; // false
type Result2 = EqEq<"abc", "abc">; // true
type Result3 = EqEq<true, false>; // false
type Result4 = EqEq<never, never>; // true
  • 模式匹配和 infer 引用
1
2
3
type Tuple = ['ji', 'ni', 'tai', 'mei']
type Result1 = Tuple extends [infer First, ...infer Rest] ? First : never // 'ji'
type Result2 = Tuple extends [infer First, ...infer Rest] ? Rest : never // ['ni', 'tai', 'mei']

元组的基本体操

1
2
3
// 扩展元组
type A = [1]
type B = [...A, 2]
1
2
3
4
5
6
7
// 获取元组最后一项
type A = [1, 2, 3, 4]
type Last<T extends unknown[]> = T extends [...unknown[], infer Last] ? Last : never
type Result = Last<A>

// 不能用 JS 的思维实现,下面这样是错的
type Last<T extends unknown[]> = T[T['length'] - 1] // TS 中没有提供减法操作,4 - 1 在 TS 中无法计算
1
2
3
4
5
6
7
// 获取元组除了最后一项的其他项
type A = [1, 2, 3, 4]
type NotLast<T extends unknown[]> = T extends [...infer X, unknown] ? X : never
type Result = NotLast<A>

// 不能用 JS 的思维实现,下面这样是错的
type Last<T extends unknown[]> = T[T['length'] - 1] // TS 中没有提供减法操作,4 - 1 在 TS 中无法计算

字符串的基本体操

1
2
3
4
5
6
// 大小写切换
type A = 'tracy'
type B = Capitalize<A> // 'Tracy'

type C = 'ji' | 'ni' | 'tai' | 'mei'
type D = Capitalize<C> // 联合类型在泛型中会按分配律计算,结果为 'Ji' | 'Ni' | 'Tai' | 'Mei'

除了 Capitalize 首字母大写外,TypeScript 还内置了 Uppercase 全变成大写,Uncapitalize 首字母小写,Lowercase 全变成小写。

1
2
3
4
5
6
// 模板字符串
type A = 'ji'
type B = 'ni'
type C = 'tai'
type D = 'mei'
type X = `${A} ${B} ${C} ${D}` // 'ji ni tai mei'
1
2
3
4
5
6
7
8
9
10
// 转字符串
// 将类型转为字符串有一定的限制,仅支持下面的类型
type CanStringified = string | number | bigint | boolean | null | undefined
// 将支持的类型转化为字符串
type Stringify<T extends CanStringified> = `${T}`

type Result1 = Stringify<0> // '0'
type Result2 = Stringify<-1> // '-1'
type Result3 = Stringify<0.1> // '0.1'
type Result4 = Stringify<'0.2'> // '0.2'
1
2
3
4
// 获取第一个字符
type A = 'ji ni tai mei'
type First<T extends string> = T extends `${infer F}${string}` ? F : never
type Result = First<A> // 'j'

注:使用模式匹配只能获取第一个字符和其它剩下字符,不能获取最后一个字符,如果想要获取最后一个字符,可转为元组操作。

1
2
3
4
5
6
7
8
// 获取最后一个字符
type A = 'ji ni tai mei'
type LastOfTuple<T extends unknown[]> = T extends [...infer _, infer L] ? L : never
type StringToTuple<S extends string> = S extends `${infer F}${infer R}`
? [F, ...StringToTuple<R>]
: []
type LastOfString<S extends string> = LastOfTuple<StringToTuple<S>>
type Result = LastOfString<A>
1
2
3
4
5
// 字符串转联合类型
type StringToUnion<S extends string> = S extends `${infer First}${infer Rest}`
? First | StringToUnion<Rest>
: never
type Result = StringToUnion<'jinitaimei'> // "j" | "i" | "n" | "t" | "a" | "m" | "e",联合类型会自动去重
1
2
3
4
5
// 字符串转元组
type StringToTuple<S extends string> = S extends `${infer First}${infer Rest}`
? [First, ...StringToTuple<Rest>]
: []
type Result = StringToTuple<'jinitaimei'> // ["j", "i", "n", "i", "t", "a", "i", "m", "e", "i"]

Type-Challenges

Type-Challenges 中常见问题解法。

1
2
3
4
5
6
7
8
9
10
11
// 0004 Pick
type Pick<T, Key extends keyof T> = {
[K in Key]: T[K]
}

type Result = Pick<{a: 1; b: 2; c: 3}, 'a'>

// 可使用代入法来思考泛型
{
[K in 'a']: {a: 1; b: 2; c: 3}['a'] // 即 'a': 1
}
1
2
3
4
5
6
// 3321 Parameters
// 获取函数参数类型
type Parameters<F extends (...args: any[]) => any> = F extends (...args: infer X) => any ? X : never

const foo = (arg1: string, arg2: number): void => {}
type Result = Parameters<typeof foo>
1
2
3
4
5
6
7
8
// 0189 Awaited
// 获取 Promise resove 的数据类型
// 注意,不能直接使用 Promise 类型,否则 Thenable 用例不能通过
type Awaited<P extends PromiseLike<any>> = P extends PromiseLike<infer X>
? X extends PromiseLike<any>
? Awaited<X>
: X
: never
1
2
3
4
5
6
7
// 4471 Zip
// 像拉拉链一样将元组交叉组合
type Zip<A extends any[], B extends any[]> = A extends [infer AFirst, ...infer ARest]
? B extends [infer BFirst, ...infer BRest]
? [[AFirst, BFirst], ...Zip<ARest, BRest>]
: []
: []
1
2
3
4
5
6
7
8
9
10
11
// 4484 IsTuple
// 判断是否为元组
// 元组的长度是固定的,length 返回固定值,而数组长度是不固定的,length 返回 number
// 泛型中的 never 会直接返回 never,为了使其返回 true false,这里使用 [] 包裹,当然使用 {} 或函数类型包裹效果也一样
type IsTuple<T> = [T] extends [never]
? false
: T extends readonly any[]
? number extends T['length'] // 如果是数组则返回 false
? false
: true
:false
1
2
3
4
5
6
7
// 5310 Join
// 将元组使用分隔符连接成字符串
type Join<T extends string[], U extends string | number> = T extends [infer First extends string, ...infer Rest extends string[]]
? Rest['length'] extends 0
? First
: `${First}${U}${Join<Rest, U>}`
: ''