泛型
示例代码:typescript-demos
泛型的重要性
“泛型是 TypeScript 中非常重要的一个概念,因为在之后实际开发中任何时候都离不开泛型的帮助,原因就在于泛型给予开发者创造灵活、可重用代码的能力。”1
“泛型是 TypeScript(以下简称 TS) 比较高级的功能之一,理解起来也比较困难。泛型应用场景非常广泛,很多地方都能看到它的影子。平时我们阅读开源 TS 项目源码,或者在自己的 TS 项目中使用一些第三方库(比如 React)的时候,经常会看到各种泛型定义。如果你不是特别了解泛型,那么你很可能不仅不会用,不会实现,甚至看不懂这是在干什么。”2
泛型是什么
“软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。”3
适用场景
两个参考标准
- 当 你的函数、接口或类将处理多种数据类型时
- 当函数、接口或类在多个地方使用该数据类型时
示例 1
首先我们来定义一个 通用 的 identity
函数,接收一个参数并直接返回它:
function identity(value) {
return value;
}
假如设定参数 value
为 number
类型,那么当传 string
就没用了。那很简单,改为 any
类型不就行了?这样当然也不行:
在静态编写的时候并不确定传入的参数到底是什么类型,只有当在运行时传入参数我们才能确定,这就导致了:
- 失去了定义应该返回哪种类型的能力
- 编译器失去了类型保护的作用
那么我们就需要变量,这个变量代表了传入的类型,然后再返回这个变量,它是一种特殊的变量,只用于表示类型而不是值。这个类型变量在 TS 中就叫做 泛型:
function returnItem<T>(params: T): T {
return params;
}
在函数名称后面声明泛型变量 <T>
,它用于捕获开发者传入的参数类型,然后就可以使用 T
作为参数类型和返回值类型。
所以,上述问题的解决方法就是使用泛型,写法如下:
function identity <T>(value: T): T {
return value;
}
console.log(identity(123)); // OK
console.log(identity('abc')); // OK
VSC 光标悬停调用函数名称,效果如下:
当我们调用
identity<number>(123)
时,number
类型就会像参数一样,在出现T
的任何位置填充该类型。
说明
<T>
中的T
被称为 类型变量,是我们希望传递给函数的类型占位符T
代表 Type,在定义泛型时通常用作第一个类型变量名称T
可以用任何有效名称代替,常见泛型变量代表还有:K
(Key):表示对象中的键类型V
(Value):表示对象中的值类型E
(Element):表示元素类型
示例 2
如何引入希望定义的任何数量的类型变量?
例如引入一个新的类型变量 U
,用于扩展我们定义的 identity
函数:
function identity<T, U>(value: T, message: U): T {
console.log(message);
return value;
}
console.log(identity<number, string>(68, "Tony")); // OK
注意到上面返回类型只有 T
,那么如果要返回两种类型呢?——使用 元组。
function identity<T, U>(value: T, message: U): [T, U] {
console.log(message);
return [value, message];
}
console.log(identity<number, string>(68, "Tony")); // OK
示例 3
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
swap([7, 'seven']); // => ['seven', 7]
除了元组还有其他更好的方案?——使用 泛型接口。
泛型接口
示例
// 定义一个用于 `identity` 函数的通用接口 `Identities`
interface Identities<V, M> {
value: V,
message: M
}
function identity<T, U> (value: T, message: U): Identities<T, U> {
console.log(value + ': ' + typeof(value));
console.log(message + ': ' + typeof(message));
let identities: Identities<T, U> = {
value,
message
}
return identities;
}
console.log(identity(68, 'Tony')); // OK
泛型类
泛型除了可以应用在函数和接口外,也可以应用在类中,还可以作用于类的成员函数。
在类中使用泛型,只需在类名后面,使用 <T, ...>
的语法定义任意多个类型变量:
interface GenericInterface<U> {
value: U,
getIdentity: () => U
}
// `IdentityClass` 实现 `GenericInterface<T>`,
// 当 `T` 表示 `number` 类型时,等于 `IdentityClass` 实现了 `GenericInterface<number>`
class IdentityClass<T> implements GenericInterface<T> {
value: T;
constructor(value: T) {
this.value = value;
}
getIdentity(): T {
return this.value;
}
}
const myNumberClass = new IdentityClass<number>(68);
console.log(myNumberClass.getIdentity()); // => 68
const myStringClass = new IdentityClass<string>('Tony');
console.log(myStringClass.getIdentity()); // => Tony
泛型约束与类型索引
泛型约束
泛型约束作用:限制每个类型变量接受的类型数量。
两种常见场景
- 确保属性存在
- 检查对象上的键是否存在
确保属性存在
例如,当我们处理字符串或数组时,假设 length
属性可用:
function identity<T>(arg: T): T {
console.log(arg.length); // Error, 类型“T”上不存在属性“length”
return arg;
}
这种情况下,编译器不知道 T
是否包含 length
属性,因为我们可以把任何类型赋值给 类型变量 T
。那么该如何做才能让 TS 编译器相信 T
有 length
属性呢?有以下两种处理方式:
方式 1 - 继承属性接口类型
interface Length {
length: number;
}
// 让 `T` 实现继承接口类型 `Length`
function identity<T extends Length>(arg: T): T {
console.log(arg.length);
return arg;
}
// identity(68); // Error, 类型“number”的参数不能赋给类型“Length”的参数。
identity('string'); // OK
identity([1, 2, 3]); // OK
我们还可以用
,
号来分隔多种约束类型,如<T extends Length, Type2, Type3>
。
方式 2 - 显式设置特定类型
function identity<T>(arg: T[]): T[] {
console.log(arg.length);
return arg;
}
// or
function identity<T>(arg: Array<T>): Array<T> {
console.log(arg.length);
return arg;
}
// identity(68); // Error, 类型“number”的参数不能赋给类型“unknown[]”的参数。
identity([1, 2, 3]);
检查对象上的键是否存在
TS v2.1
引入一个 keyof
操作符,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型
interface Person {
name: string,
age: number,
location: string,
}
type K1 = keyof Person; // `name` | `age` | `lacation`
type K2 = keyof Person[]; // `name` | `legnth` | `push` | `concat` | ...
type K3 = keyof { [x: string]: Person }; // `string` | `number`
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
使用示例
enum Difficulty {
Easy,
Intermediate,
Hard
}
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
let tsInfo = {
name: 'TS',
superSetOf: 'JS',
difficulty: Difficulty.Intermediate
}
// 获取 `tsInof` 的 `difficulty` 属性值
let difficulty: Difficulty = getProperty(tsInfo, 'difficulty'); // OK
// 获取 `tsInof` 的 `superSetOf` 属性值
// let superSetOf: string = getProperty(tsInfo, '_superSetOf'); // Error
VSCode 悬停可以看到具体报错信息,类型错误捕获成功
很明显通过使用泛型约束,在编译阶段我们就可以提前发现错误,大大提高了程序的健壮性和稳定性。
泛型索引
问题:设计一个函数,这个函数接收两个参数,一个参数为对象,另一个参数为对象上的属性,通过这两个参数返回这个属性的值
function getValue(obj: object, key: string) {
return obj[key]; // Error, 类型为 "string" 的表达式不能用于索引类型 "{}"
}
TS 编译器告诉我们,参数 obj
实际上是 {}
,因此后面的 key
是无法在上面取得任何值,那么我们可以 extends
一个对象类型(T extends object
):
function getValue<T extends object>(obj: T, key: string) {
return obj[key]; // Error, same tips
}
还是报错,因为 object
类型本质上还是 {}
,无法确定第二个参数 key
是不是存在于 obj
上,所以我们需要用 key
来进一步约束 obj
。
可以借助索引类型 <U extends keyof T>
:用索引类型 keyof T
把传入的对象的属性类型取出生成一个联合类型,这里的泛型 U
就会被约束在这个联合类型中,这样一来函数就被完整定义了:
function getValue<T extends object, U extends keyof T>(obj: T, key: U) {
return obj[key]; // OK
}
const person = { name: 'Tony', age: 18 };
getValue(person, 'age'); // OK
在这里,getValue
第二个参数 key
类型被约束到一个联合类型 name | id
,他只能是这两个之一,至此你就能获取到良好的类型提示了:
多重类型的泛型约束
上面只是单一类型对泛型进行约束,那么如果允许被两个接口类型约束呢?
interface FirstInterface {
dosomething(): number
}
interface SecondInterface {
dosomethingElse(): string
}
class Demo<T extends FirstInterface, SecondInterface> {
private genericProperty!: T;
useT() {
this.genericProperty.dosomething(); // OK
// this.genericProperty.dosomethingElse(); // Error, 类型“T”上不存在属性“dosomethingElse”
}
}
如上所示,只有 FirstInterface
约束了泛型 T
,SecondInterface
并没有生效,那么怎么用两个接口 同时约束 泛型呢?
如果这样呢?
class Demo<T extends FirstInterface, T extends SecondInterface> { // Error, 标识符“T”重复。
// ...
}
也不行。
既然泛型“只能被一个接口类型约束”,那么我们可以弄个子接口类型 childInterface
,然后继承 FirstInterface
和 SecondInterface
,不就可以吗?
interface FirstInterface { dosomething(): number };
interface SecondInterface { dosomethingElse(): string };
interface childInterface extends FirstInterface, SecondInterface {}; // here
class Demo<T extends childInterface> {
private genericProperty!: T;
useT() {
this.genericProperty.dosomething(); // OK
this.genericProperty.dosomethingElse(); // OK
}
}
成功了。实际上,我们也可以利用交叉类型来进行 多类型约束:
interface FirstInterface { dosomething(): number };
interface SecondInterface { dosomethingElse(): string };
class Demo<T extends FirstInterface & SecondInterface> { // here
private genericProperty!: T;
useT() {
this.genericProperty.dosomething(); // OK
this.genericProperty.dosomethingElse(); // OK
}
}
泛型参数默认类型
TS v2.3
以后,泛型的类型参数也可以指定默认类型,语法:<T=Default Type>
。
当使用泛型时,没有在代码中直接指定类型参数,从实际值参数中也无法推断出类型时,这个默认类型就会起作用。
示例
interface A<T = string> { name: T } // 泛型接口A默认为 `string` 类型
const strA: A = { name: 'Tony' } // OK
const numB: A<number> = { name: 1000 } // OK
泛型条件类型
TS v2.8
引入了条件类型,可以根据某些条件(类型兼容性约束)得到不同的类型。
示例 1
条件类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一
// 若 `T` 能够赋值给 `U`,那么类型是 `X`,否则为 `Y`
T extends U ? X : Y
示例 2
在条件类型表达式中,通常还会结合 infer
关键字,实现类型抽取
// 定义一个泛型接口Dictionary;T默认为类型为any;键为string,值为T
interface Dictionary<T = any> {
[key: string]: T
}
// 用类型别名 `type` 并基于 `Dictionary` 弄了一个新类型,名为 `StrDict`
type StrDict = Dictionary<string>;
// ???
type DictMember<T> = T extends Dictionary<infer V> ? V : never;
// ???
type StrDictMember = DictMember<StrDict>;
示例 3
利用条件类型和 infer 关键字,可以方便地实现获取 Promise 对象的返回值类型
interface Person {
name: string,
age: number
}
async function stringPromise() {
return 'Hello, Tony';
}
async function personPromise() {
return { name: 'Tony', age: 30 } as Person;
}
type PromiseType<T> = (args: any[]) => Promise<T>;
type UnPromiseify<T> = T extends PromiseType<infer U> ? U : never; // `infer` ???
type extractStringPromise = UnPromiseify<typeof stringPromise>;
type extractPersonPromise = UnPromiseify<typeof personPromise>;
泛型工具类
为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial
、Required
、Readonly
、Record
和 ReturnType
等。
Partial
定义
// node_modules/typescript/lib/lib.es5.d.ts
/**
* Make all properties in T optional
* 将某个类型里的属性全部变为可选项 `?`
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
示例
interface Todo {
title: string,
description: string
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}
const todo1 = {
title: 'organize desk',
description: 'clear clutter'
}
const todo2 = updateTodo(todo1, {
description: 'throw out trash'
})
console.log(todo1); // => {title: "organize desk", description: "clear clutter"}
console.log(todo2); // => {title: "organize desk", description: "throw out trash"}
说明
Record
定义
// node_modules/typescript/lib/lib.es5.d.ts
/**
* Construct a type with a set of properties K of type T
* 将 `K` 中所有的属性的值转化为 `T` 类型
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};
示例
interface PageInfo {
title: string,
}
type Page = `home` | `about` | `concact`;
const x: Record<Page, PageInfo> = {
about: { title: 'about' },
concact: { title: 'concact' },
home: { title: 'home' }
}
Pick
定义
// node_modules/typescript/lib/lib.es5.d.ts
/**
* From T, pick a set of properties whose keys are in the union K
* 将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
示例
interface Todo {
title: string,
description: string,
completed: boolean
}
type TodoPreview = Pick<Todo, 'title' | 'completed'>;
const todo: TodoPreview = {
title: 'Clean room',
completed: false
}
Exclude
定义
// node_modules/typescript/lib/lib.es5.d.ts
/**
* Exclude from T those types that are assignable to U
* 将某个类型中属于另一个的类型移除掉
*/
type Exclude<T, U> = T extends U ? never : T;
示例
type T0 = Exclude<'a' | 'b', 'a'>; // 'b' | 'c'
type T1 = Exclude<'a' | 'b' | 'c', 'a' | 'b'>; // 'c
type T2 = Exclude<string | number | (() => void), Function>; // string | number
ReturnType
定义
// node_modules/typescript/lib/lib.es5.d.ts
/**
* Obtain the return type of a function type
* 获取函数 `T` 的返回类型
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
示例
type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; //void
type T2 = ReturnType<<T>() => T>; // {};???
type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[];障眼法?
type T4 = ReturnType<any>; // any
type T5 = ReturnType<never>; // any
// type T6 = ReturnType<string>; // Error: 类型“string”不满足约束“(...args: any) => any”。
// type T7 = ReturnType<Function>; // Error: 类型“Function”不满足约束“(...args: any) => any”
想了解更多,可以参考以下资料:
- piotrwitek/utility-types(源码库)
- TypeScript 3.7 Utility Types | Printable PDF Cheat Sheet
- 掌握 TS 这些工具类型,让你开发事半功倍
- TypeScript 强大的类型别名
- Axes Blog
使用泛型创建对象
问题:
- 如何使用泛型来创建对象?
- 什么情况下需要使用泛型来创建对象?
问题
示例
class FirstClass {
id: number | undefined;
}
class SecondClass {
name: string | undefined;
}
class GenericCreator<T> {
create(): T {
return new T(); // Error: “T”仅表示类型,但在此处却作为值使用。
}
}
const creator1 = new GenericCreator<FirstClass>();
const firstClass: FirstClass = creator1.create();
const creator2 = new GenericCreator<SecondClass>();
const secondClass: SecondClass = creator2.create();
问题
- 这段代码无法运行,因为报错了
- 为了使通用类能够创建
T
类型的对象,需要通过其构造函数来引用T
类型,那么该如何实现?
要解决这个问题,需要先了解:
- 构造函数签名
- 构造函数类型
构造函数签名
在 TS 接口中,可以使用 new
关键字来描述一个构造函数
interface Point {
new (x: number, y: number): Point; // 构造签名
}
构造签名语法
// new 可选类型参数 可选参数列表 可选类型注解
new TypeParametersopt ( ParameterListopt ) TypeAnnotationopt
常见形式
new C
new C ( ... )
new C < ... > ( ... )
构造函数类型
描述
- 包含一个或多个构造签名的对象类型被称为构造函数类型
- 构造函数类型可以使用 构造函数类型字面量 或 包含构造签名的对象类型字面量 来编写
构造函数类型字面量形式
new < T1, T2, ... > ( p1, p2, ... ) => R
包含构造签名的对象类型字面量
{ new < T1, T2, ... >( p1, p2, ... ) : R }
示例
new (x: number, y: number) => Point // 构造函数类型字面量形式
// 等价于
{ new (x: number, y: number): Point } // 包含构造签名的对象类型字面量
构造函数类型的应用
错误示例
interface Point {
new (x: number, y: number): Point,
x: number,
y: number
}
class Point2D implements Point { // Error: 类“Point2D”错误实现接口“Point”
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const point: Point = new Point2D(1, 2);
问题
- 类
Point2D
错误实现接口Point
- 需要把前面定义的
Point
接口中的属性和构造函数类型进行分离
正确示例
interface Point {
x: number,
y: number
}
interface PointConstructor {
new (x: number, y: number): Point
}
class Point2D implements Point {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
// 工厂函数
function newPoint(
pointConstructor: PointConstructor,
x: number,
y: number
): Point {
return new pointConstructor(x, y);
}
const point: Point = newPoint(Point2D, 1, 2);
使用泛型创建对象
了解完 构造函数签名 和 构造函数类型 后,就可以来解决开头的问题
class FirstClass {
id: number | undefined;
}
class SecondClass {
name: string | undefined;
}
class GenericCreator<T> {
// 重构 `create` 方法
create<T>(c: { new (): T }): T {
return new c();
}
}
const creator1 = new GenericCreator<FirstClass>();
const firstClass: FirstClass = creator1.create(FirstClass);
const creator2 = new GenericCreator<SecondClass>();
const secondClass: SecondClass = creator2.create(SecondClass);
想了解更多,可以参考
- How to Finally Understand Generics in TypeScript | by Jim Rottinger
- TypeScript tip of the week - generics
- 泛型(generic)的妙用
- TypeScript:得泛型者,得天下
- typescript 中的 interface 和 type 到底有什么区别?
- TypeScript中的条件类型和infer关键字
- TypeScript 入门教程
- TypeScript infer 关键字 | 全栈修仙之路
- 理解TypeScript中的infer关键字