泛型编程(上)

泛型就像函数

函数

1
2
const f = (a, b) => a + b
const result = f(1, 2) // 3

泛型

1
2
type F<A, B> = A | B
type Result = F<string, number> // string | number
泛型就像函数
泛型就像函数

函数的本质是什么

函数的本质是推后执行的、部分待定的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 立即执行
console.log(1)

// 推后一步执行
const f1 = () => console.log(1)
f1()

// 推后两步执行
const f2 = () => console.log(1)
console.log(2)
f2()

// 部分待定的
const f3 = (fn, n) => fn(n) // 参数待定
f3(console.log, 3)

泛型的本质是什么

泛型的本质是推后执行的、部分待定的类型

没有泛型的类型系统,就如同没有函数的编程语言

什么时候需要泛型

当我们需要准确的定义返回值的类型的时候,就需要泛型

这时候你可能会说,做类型收窄不就行了吗?就像下面这样

1
2
3
4
5
6
7
8
9
10
function echo(whatever: number | string | boolean) {
switch (typeof whatever) {
case 'number':
return whatever;
case 'string':
return whatever;
case 'boolean':
return whatever;
}
}

但是你会发现,这样写得到的返回值类型是不准确的

1
2
3
const a = echo(233);
// 我们期望 a 的类型是 233
// 但 a 的类型却是 string | number | boolean

这时候,泛型就派上用场了

1
2
3
4
5
6
function echo<T>(whatever: T): T {
return whatever;
}

const a = echo(233);
// a 的类型为 233

简单泛型示例

1
2
3
4
5
6
7
8
9
10
type Union<A, B> = A | B;
type Union3<A, B, C> = A | B | C;
type Intersect<A, B> = A & B;
type Intersect3<A, B, C> = A & B & C;
interface List<A> {
[index: number]: A;
}
interface Hash<V> {
[key: string]: V;
}

条件类型(Conditional Types)

T extends string 可理解为 T <= stringT 包含于 string

1
2
3
4
5
6
type R1 = LikeString<'hi'> // true
type R2 = LikeString<true> // false
type S1 = LikeNumber<2333> // 1
type S2 = LikeNumber<false> // 2
type T1 = LikePerson<{ name: 'ClariS', age: 18 }> // yes
type T2 = LikePerson<{ age: 1 }> // no

泛型中的特殊运算规则

现有如下泛型 ToArray

1
type ToArray<T> = T extends unknown ? T[] : never;

问:type Result = ToArray<string | number> 的类型是什么?

直接说答案:Resultstring[] | number[]

你可能会疑惑,按照一般的理解,string | number 是包含于 unknown 的,那么 Result 就应该是 (string | number)[]

按照通常的理解是没错,但因为这是在泛型中,因此规则会有些许不同

可以按照如下拆分过程来进行理解记忆:

1
2
3
4
type Result = ToArray<string | number>;
// type Result = (string | number) extends unknown ? ...
// type Result = (string extends unknown ? ...) | (number extends unknown ? ...)
// type Result = string[] | number[]

即泛型中的联合类型会分开进行运算extends 运算),这就好像是乘法中的分配率 (A + B) X C = A X C + B X C

再问:type Result2 = ToArray<never> 的类型是什么?

答案是 never

同样按照一般的理解,通常情况下(非泛型中),never 是包含于 unknown 的,那么 Result 应该是 never[] 才对

Result 在此处却为 never

1
type Result = ToArray<never>; // never

即泛型中的 never 进行任何运算(extends 运算或者 & 运算)都只会得到 never,这就好像是乘法中的零 0 X C = 0

注意:以上讨论的规则只对泛型有效

在非泛型中,使用 extends 运算会得到不一样的结果

1
2
3
4
5
6
7
8
// 非泛型
type T0 = never extends never ? true : false
// ^? type T0 = true

// 泛型
type IsNever<T> = T extends never ? true : false
type T1 = IsNever<never>
// ^? type T1 = never

那么,如何才能禁用泛型中的自动拆分规则呢?

我们可以构造一个类型,在外面包裹上一个新的类型来防止 TypeScript 遍历 extends 左侧类型的行为,比如下面的 []

1
2
3
4
5
6
7
8
9
type IsNever<T> = [T] extends [never] ? true : false
type T0 = IsNever<never>
// ^? type T0 = true

type ToArray<T> = [T] extends unknown ? T[] : never;
type Result1 = ToArray<string | number>;
// ^? type Result1 = (string | number)[]
type Result2 = ToArray<never>;
// ^? type Result2 = never[]

在泛型中使用 keyof

keyof 关键字用于获取对象类型的所有键的联合类型

1
2
3
4
type Person = { name: string; age: number };
type GetKeys<T> = keyof T;
type Result = GetKeys<Person>;
// ^-- 'name' | 'age'

常和映射类型一起使用

1
2
3
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};

keyof 也可用于数组 / 元组

1
2
type A = keyof [1, 2, 3];
// A 的类型为 "0" | "1" | "2" | "length" | "toString" | "pop" | ...

在泛型中使用 extends keyof

1
2
3
4
5
type Person = { name: string; age: number };
type GetKeyType<T, K extends keyof T> = T[K];
type Result1 = GetKeyType<Person, 'name'>; // string
type Result2 = GetKeyType<Person, 'age'>; // number
type Result3 = GetKeyType<Person, 'name' | 'age'>; // string | number

K extends keyof T 的写法称为泛型约束GetKeyType 第二个参数 k 的类型必须包含于 string | number

其他

PropertyKey

PropertyKey 是 TS 中的一个内置类型,它表示可以用作对象属性键的类型。它是 string | number | symbol 类型的别名,因为这些类型都可以用作对象属性的键。

PropertyKey 类型常用于泛型约束,以确保泛型类型参数只能是可以用作对象属性键的类型。例如,定义一个泛型类型 Foo<K extends PropertyKey>,其中 K 只能是 stringnumbersymbol 类型。

T[number]

1
2
type A = [1, 2, 3]
type B = A[number] // 3 | 1 | 2,无序

A[number] 表示获取类型 A 的数字索引签名的类型。由于元组类型具有隐式的数字索引签名,因此 A[number] 的类型为元组中所有元素类型的联合类型 1 | 2 | 3


(●'◡'●)ノ♥