TypeScript泛型编程一览
对于初学者来说,TypeScript 可能只是为我们所定义的各种变量添加一个类型而且,实际上相对于 JavaScript 来说变化并不大。但是会经常遇到类型提示不正确,不准确的问题,并且书写过程中无法灵活应用,总是复制来复制去,深入了解下去以后,个人觉得泛型才是 TypeScript 中的精髓,但它并不是简单的复用类型,要想灵活应用,市面上却很少有关于这方面的教程,文档也是语焉不详。
偶然在网上找到了一个泛型编程的练习,做完了上面所有简单和中等的题目,感慨泛型其实和普通的语法非常相似,于是站在这个角度来看问题的时候,把泛型看作是编程语言看待,或许会更好理解。
关键字
keyof
keyof 用来遍历接口类型的键名,即将接口类型转换为联合类型
interface Type {
name: string;
age: number;
sex: string;
}
type Person = keyof Type;
// p只能取接口的键,也就是name或者是age
const p:Person = 'name';
// const p:Person = 'age'
in
in 关键字用来遍历联合类型,通常只能在接口类型中进行遍历
type Union = 'name' | 'age' | 'sex';
type ForEach = {
[K in Union]: K
};
// 得到的结果是这样的
// {
// name: 'name',
// age: 'age',
// sex: 'sex'
// }
extends
extends 关键字在泛型中有两种含义,一种就是继承关系,另外一种则是约束关系,对于泛型参数的一些限制条件,同时也可以作为判断,也就是下面说到的条件判断的基础。
// 当使用某个泛型T,并且希望T具备某些能力,诸如数组能力、字符串能力,对象的某个属性的能力
interface Animal {
eat: (name: string) => void
}
type Dog<T extends Animal> = {}; // 表示传递给Dog的类型T必须具备Animal上的属性
infer
这里 infer 放到最后是因为 infer 是 TypeScript 特有的关键字,也是泛型编程中才有的,从字面上解释,infer 就是推断的意思,可以理解为推断类型。
type Foo = (name: string) => number;
type Param<T> = T extends (name: infer P) => any ? P : never;
type Return<T> = T extends (...args: unknown[]) => infer R ? R : never;
// 这里其实就是内置类型Parameters<T>和ReturnType<T> 的实现
type Result = Param<Foo>; // string
type Result1 = Return<Foo>; // number
// 推导数组
type Arr = [1,'str',false,Symbol];
type First<T> = T extends [infer F, ...infer Other] ? F : never;
type Last<T> = T extends [...infer Other, infer L] ? L : never;
type Result2 = First<Arr>; // 1
type Result3 = Last<Arr>; // Symbol
// 推导字符串
type Str = 'abcd';
type Capital<T extends string> = T extends `${infer F}${infer Other}` ? `${Uppercase<F>}${Other}` : T;
type Result4 = Capital<Str>; // Abcd
as
as 在泛型编程中使用的比较少,也是作为类型断言的功能来使用的,下面看个例子
interface Foo {
[key: string]: any;
foo(): void;
}
type IndexSignature<T> = {
[P in keyof T as string]: T[P]
};
type Result = IndexSignature<Foo>; // { [key: string]: any }
as 就是针对这种动态键,加上了as string
才能选中[key: string]
,否则的话,选中的还是全部的键值对。
修饰符-
修饰符是指在接口类型的键名前面添加的修饰性的词,诸如 readonly、可选属性这种。实际上 TypeScript 中虽然有提到,但是并没有说明这种叫什么,所以我就自作主张,类比编程语言,就用了修饰符
这个词。
有修饰,就有取消修饰,针对 readonly、required 这种,怎么去取消只读和必须的修饰呢,可以使用“-”这个符号,我自己称之为取反修饰符。又是一个在 TypeScript 文档中没有提到的内容。
interface Person {
readonly name: string;
readonly age: number;
}
type CancelOnly<T> = {
-readonly [K in keyof T]: T[K]
};
type Test = CancelOnly<Person>; // { name: string; age: nunber }
// 对于可选属性也是一样,使用-符号取消可选
interface Person1 {
name?: string;
age?: number;
}
type CancelOption<T> = {
[K in keyof T]-?: T[K]
};
type Test1 = CancelOption<Person1>; // { name: string; age: nunber }
内置类型
内置类型就是 TypeScript 中存在的类型,相当于 JavaScript 中的 Boolean 这种不需要定义的类型,内置类型非常多,这里只讲解一些常用内置类型的实现,加深对泛型编程的理解。
- Partial 就是将接口类型中的所有键变成可选,实现如下:
type MyPartial<T> = {
[K in keyof T]?: T[K]
};
- Required 和上面的 Partial 刚好相反,Partial 类型是将接口中的键都变成可选类型,而 Required 则将所有的键变成必选,也就是去掉了
?
的标记。
// 这里使用了上面提到的修饰符-,取消可选
type MyRequired<T> = {
[K in keyof T]-?: T[K]
}
- Readonly 顾名思义就是将接口类型中所有的键变成只读类型,同理,还有类似 Required 都是一个实现原理
type MyReadonly<T> = {
readonly [K in keyof T]: T[K]
};
- Pick<T, U> 从 T 中选择 U 指定的键值对组成一个新类型
type MyPick<T, U extends keyof T> = {
[K in U]: T[K]
};
- Exclude<T, U> 就是将 U 类型从 T 中排除,只有联合类型才能作为 T 类型使用。可以理解为 Pick 类型的取反,但是类型参数是不一样的。
interface Person {
name: string;
age: number;
height: number;
}
type MyExclude<T, U> = T extends U ? never : T;
// 直接这么去除是无效的
type Test = MyExclude<Person, 'name'>;
// Pick类型则是可以的
type Test1 = MyPick<Person, 'name'>; // { name: string }
// Exclude 可以把T类型变成联合类型
type Test2 = MyExclude<keyof Person, 'name'>; // 得到的结果也是联合类型:'age' | 'height',并不是我们想要的排除的效果
// 可以遍历所有的键,如果键继承了给定的U类型,则排除掉,这样就留下了剩下的键
type IExclude<T, U> = {
[K in keyof T as K extends U ? never : K]: T[K]
}
type Test3 = IExclude<Person, 'name'>; // { age: number; height: number }
- Omit<T, U> 从 T 中删除 U 类型,可以理解为从借口中删除指定的键
interface Todo {
title: string
description: string
completed: boolean
}
type MyOmit<T, U> = {
[K in keyof T as K extends U ? never : K]: T[K]
}
type Test = MyOmit<Todo, 'description' | 'title'>; // { title: string }
// 结合上面的内置类型,可以发现排除某个键,可以使用 Exclude 来实现
type MyOmit1<T, U> = Pick<T, Exclude<keyof T, 'description' | 'title'>>
// 这里要注意 Exclude 接受的是一个联合类型,因此需要对 T 进行转换
自定义类型
基础类型
常见的基础类型,string | number | boolean | Symbol 以外,还有一些 TypeScript 中特有的类型,如 any、unknown、never、void。
复杂类型
- 接口类型
- 联合类型
到这里可以把我们的泛型看作是一个函数,诸如 T,K 这种标识符实际上就是我们的函数形参,而实际传递进去的上面提到的这些具体类型就是实参了。
// 以内置类型Pick为例
type Pick<T, U> = {
[K in U]: T[K]
}
// 可以看成这样
// 这里只是一种假想,是为了便于理解泛型,实际底层可能并不是如此
function Pick (T, U) {
let res = {}
for (let K in U) {
res[K] = T[K]
}
return res
}
类型转换
- 接口类型转为联合类型
使用 keyof 关键字就可以将接口类型转换为联合类型。而将联合类型转换为接口类型,则可以使用关键字 in。上文已经提到过,这里就不在赘述。
- 元组类型转其他类型
type Tuple = ['name','age','sex'];
// 元组类型转为联合类型
type Tuple2Union<T extends unknown[]> = T[number];
// 元组转为对象类型
type TupleToObject<T extends any[]> = {
[P in T[number]]: P
};
type resullt = TupleToObject<Tuple>;
// {
// a: "a";
// b: "b";
// c: "c";
// d: "d";
// }
元组可以转成联合类型,那联合类型如何转成元组呢?实际上在尝试以后发现是无法进行转换的,查询了很多资料以后发现,有种解释是比较合理的
联合类型表示的是所有类型中的一种,而元组包含了所有类型,从或到与的关系是冲突的,所以无法进行转换。
- 字符串类型转换其他类型
// 字符串类型转换为元组
// 这里还用到了类似于函数默认参数的类型P,用来构造元组类型
type String2Tuple<T extends string, P extends unknown[] = []> =
T extends `${infer F}${infer Rest}`
? String2Tuple<Rest, [...P, F]>
: P
type Result = String2Tuple<'abc'>; // ['a','b','c']
// 结合上面元组转成联合类型的方法,也可以把字符串转成联合类型
type String2Union<T extends string> = String2Tuple<T>[number]
流程控制
优先级
逻辑关系
interface Person {
name: string;
age: number;
sex: string;
}
interface Height {
height: number;
face: string;
name: string;
}
type And<T, U> = T & U;
type Or<T, U> = T | U;
type PersonOfKeys = keyof Person;
type HeightOfKeys = keyof Height;
const case1: And<Person, Height> = {
name: 'aaa',
age: 20,
sex: 'female',
height: 170,
face: '😂'
}; // 与的关系
const case2: Or<Person, Height> = {name: 'aaaa', age: 123, sex: 'vvv'}; // 或的关系
const case3: And<PersonOfKeys, HeightOfKeys> = 'name'; // 交集中的元素
const case4: Or<PersonOfKeys, HeightOfKeys> = 'age'; // 并集中任意元素
遍历
- 遍历interface
使用keyof得到接口的键名
- 遍历联合类型
使用in关键字
接口interface和联合类型的互相转换
interface到联合类型,实际就是使用keyof得到interface的键名的联合类型
而联合类型转为interface,则可以这么做:
type ForEach<T extends keyof any> = {
[K in T]: string
};
type Result = ForEach<Union>;
// {
// name: string,
// age: string,
// sex: string
// }
- 遍历元组
可以把元组当作数组来理解,遍历元素实际就是取出元组中每个索引对应的值
type arr = ['a','b','c','d'];
// 把元素当作interface来理解,元素实际就是
// {
// 0: 'a',
// 1: 'b',
// 2: 'c',
// 3: 'd'
// }
// 所以也可以直接用数字来获取对应的类型
type Type = arr[0]; // 'a'
// 遍历所有的数字索引,可以用number来代替
// 注意这里的泛型约束,表示T类型必须继承任意的数组类型,才能取number类型的索引
type ForEachTuple<T extends any[]> = T[number];
type Result = ForEachTuple<arr>; // 'a' | 'b' | 'c' | 'd'
遍历元组的关键就是用number这个类型去取元素中每一项的类型,得到的结果就是一个联合类型
既然把元组当作数组来理解,那是不是可以获取元组的长度呢?
type Length<T extends any[]> = T['length'];
type Result = Length<arr>; // 4
条件判断
type IsString<T> = T extends string ? true : false;
type Result = IsString<'abc'>; // true
这种三元表达式的判断很好理解,但是还存在一些特殊情况:
首先是对于 never 类型的判断,如何判断某个类型是不是 never 类型
type IsNever<T> = T extends never ? true : false;
type A = IsNever<never>; // never
结果却大失所望,这么判断还是 never 类型,为什么会这样呢?我觉得主要是因为 never 类型本身就代表一种不存在的类型,可以类比 Error 类型,出现 never 代表程序已经出错,自然无法进行下面的判断了。
那么如何判断是否是 never 类型呢,可以对 never 进行一层包装:
type IsNever<T> = [T] extends [never] ? true : false; // true
再来看一个复杂的判断,如何判断类型是否是联合类型
type IsUnion<T> =
T[] extends (T extends unknown ? T[] : never)
? false
: true;
首先判断T extends unknown ? T[] : never
这句,利用一个中间类型看看这句得到的类型是什么样子的
type Middle<T> = T extends unknown ? T[] : never;
type case = Middle<string>; // string[]
type case1 = Middle<number|string>; // number[]|string[]
type case2 = Middle<boolean>; // false[]|true[]
如此结合上面的IsUnion
判断T
是否继承case
,如果是联合类型自然不会继承,结果也就是true
了。
但是上面还是存在了一个特例,那就是boolean
类型,经过中间泛型得到的结果和联合是一样的,因此最后返回的结果也是true
。
所以需要对 boolean 类型再次做个判断
type IsUnion<T> =
T[] extends (T extends unknown ? T[] : never)
? false
: T extends boolean
? false
: true;
递归
type Space = ' ' | '\n' | '\t';
// 可以这么理解:类型T如果存在空格,把空格摘出去,剩下的字符再递归进行trim
type Trim<T extends string> = T extends `${Space}${infer P}${Space}` ? Trim<P> : T;
type trimed = Trim<' Hello World '>; // 'Hello World'
递归理解比较简单,但是需要注意 TypeScript 中的递归也存在递归深度问题,如果超出一定的层级,则会递归失败,所以需要性能也是泛型编程要考虑的问题。
// 注意递归退出条件:类型U的长度等于给定的长度的时候,就停止递归
// 每次递归的时候将一个数添加到数组中
type ConstructTuple<T extends number, U extends number[] = []> =
U['length'] extends T
? U
: ConstructTuple<T, [number, ...U]>
type MinusOne<T extends number> = ConstructTuple<T> extends [infer _, ...infer Other] ? Other['length'] : 0;
type Zero = MinusOne<1> // 0
type Five = MinusOne<5> // 4
// type FourFive = MinusOne<45> // 超过一定的递归调用栈,会报错,递归层级过深
尾声
关于泛型编程,还是需要在平时项目中多去练习,灵活应用才关键。很多泛型构建其实不止一种方法,找到更简单,易懂的泛型构建才是泛型编程追求的目标。
后续也会将文中提到的🌰整理放入github中,因所学有限,欢迎各位大佬继续补充泛型编程中更好的用法。
转载自:https://juejin.cn/post/7063709796939071502