泛型简介
泛型程序设计是一种编程风格或编程范式,它允许在程序中定义形式类型参数,然后在泛型实例化时使用实际类型参数来替换形式类型参数,这一过程有些类似于函数形参在被函数调用时传入的实参替换。
通过泛型可以定义通用的数据结构,增加 TypeScript 代码中类型的通用性,泛型的结构如下:
<T, U, V> // <> 括号内的就是泛型,这里T, U, V 都是形参 或者 <string> // 这里是实参 string 类型
形参的名称为合法的变量标识符,通常约定首字母大写表示它是类型,一般名称使用简写 T,U,V,W
依次递增。
泛型基本应用
假设要实现一个函数其返回由传入参数组成数组,且传入参数类型保持一致,其 JS 版本代码如下:
function toArray(a, b) { return [a, b] }
使用 TypeScript 可以写成这样:
function toArray(a: number, b: number): number[] { return [a, b] }
这样写的话 toArray
函数只能传入 number
类型,不能传入 string
object
等其他类型,要想能够传入通用类型,可以使用 any
或 unknown
类型改为如下:
function toArray(a: unknown, b: unknown): unknown[] { return [a, b] }
但是,这样写就丢失了形式参数类型 a
b
要保持类型一致,返回的参数类型与形式参数类型的关联关系的约束信息。
这里传入 string
类型,返回的任意类型组成的数组
这里两个形参分别传入 number
string
类型,没有检查出来两个形参类型不同,并且返回值是任意类型组成的数组
借助泛型的话,可以这么写:
function toArray<T>(a: T, b: T): T[] { return [a, b] }
返回值的类型明确了是 stirng[]
类型
这里也可以不传递
<string>
泛型,借助 TypeScript 内置的类型推导可以根据toArray
实际传入的参数类型推导出泛型参数 T 的实际类型
当传入参数类型不一致时也有类型校验
泛型约束
泛型可以通过 extends
关键字来定义泛型约束,下面代码表示泛型 T 必须是 string
类型的子类型,即 T 可以赋值给 string
类型。
<T extends string>
给上述 toArray
方法添加泛型约束如下:
interface Point { x: number; y: number; } function toArray<T extends Point>(a: T, b: T): T[] { return [a, b] }
当传入形式参数的类型为 number
类型时不满足泛型约束会报错
基约束
每个类型参数都有基约束,与其是否在形式类型参数上定义泛型约束无关,类型参数的实际类型一定是其基约束的子类型,基约束的规则参照如下三个规则。
三个规则
规则一 如果类型参数声明了泛型约束,泛型约束为另一个类型参数 U,则类型参数 T 的基约束为类型参数 U
<T extends U>
规则二 如果类型参数声明了泛型约束,泛型约束为某一个具体类型 Type,则类型参数 T 的基约束为类型 Type
<T extends boolean>
这里泛型 T 的基约束为类型 boolean
规则三 如果类型参数没有声明泛型约束,则类型参数 T 的基约束为[[空对象类型字面量]]
"{}"
<T>
泛型函数
[[函数签名]]中带有类型参数,该函数就是泛型函数,上面定义的 toArray
函数就是一个泛型函数。
// 普通函数签名 <T>(x: T): T // 构造函数签名 new <T>(x: T): T[]
泛型接口
接口的定义中带有类型参数,该接口就是泛型接口
interface MyArray<T> extends Array<T> { first: T | undefined; last: T | undefined; }
/lib.es5.d.ts
中对 Array 泛型接口的定义节选部分如下:
interface Array<T> { /** * Gets or sets the length of the array. This is a number one higher than the highest index in the array. */ length: number; /** * Returns a string representation of an array. */ toString(): string; /** * Returns a string representation of an array. The elements are converted to string using their toLocaleString methods. */ toLocaleString(): string; /** * Removes the last element from an array and returns it. * If the array is empty, undefined is returned and the array is not modified. */ pop(): T | undefined; /** * Appends new elements to the end of an array, and returns the new length of the array. * @param items New elements to add to the array. */ push(...items: T[]): number; /** * Combines two or more arrays. * This method returns a new array without modifying any existing arrays. * @param items Additional arrays and/or items to add to the end of the array. */ concat(...items: ConcatArray<T>[]): T[]; /** * Combines two or more arrays. * This method returns a new array without modifying any existing arrays. * @param items Additional arrays and/or items to add to the end of the array. */ concat(...items: (T | ConcatArray<T>)[]): T[]; /** * Adds all the elements of an array into a string, separated by the specified separator string. * @param separator A string used to separate one element of the array from the next in the resulting string. If omitted, the array elements are separated with a comma. */ join(separator?: string): string; }
泛型类
类的定义中带有类型参数,该类就是泛型类
// 类声明 class Container<T> { constructor(private readonly data: T) {} } const a = new Container<boolean>(true); // 类表达式 const Container = class<T> { constructor(private readonly data: T) {} } const a = new Container<boolean>(true);
类的泛型类型参数不能用于类的静态成员,泛型类描述的是类的实例类型(需要实例化类时类型参数才有实际的类型),而类的静态成员是类构造函数类型的一部分,这个可以参阅下方的类类型。
类类型
类声明、类表达式相当于声明了类的实例类型、构造函数类型,类本身的类型是构造函数类型,当类作为类类型时就是实例类型。
实例类型
TypeScript 中类声明、类表达式会引入一个新的命名类型——与类同名的类类型,这正是类可以作为类型的原因。
下面 Container
可以用作类型,并且它是兼容 ContainerType
类型的,Container
和 ContainerType
类型可以看成是等价的。
可以理解为:TypeScript 内部根据类的实例属性生成了相应的接口来表示类的实例类型。
构造函数类型
在定义类时,实际上是定义了一个构造函数,ES6 中类的本质也是如此。
这里 AConstructor
接口与 Container
类型等价,并且静态属性 version
定义在构建函数类型上。
内置泛型
TypeScript 中内置了大量的工具类型,这些工具类型都是借助泛型实现根据已有类型来创造新类型的,这是对泛型最好的应用,下面看看几个最常用内置泛型工具。
Partial
Partial<T>
创造一个新的类型,并将实际类型参数 T 中所有属性变为可选属性。
Required
Required<T>
创造一个新的类型,并将实际类型参数 T 中所有属性变为必选属性。
Record
function toArray(a, b) { return [a, b] }6
创建一个新的对象类型,实际类型参数 T 为联合类型将作为新对象类型的属性,类型参数 T 为对象类型属性的类型。
Pick
function toArray(a, b) { return [a, b] }7
挑选出对象类型 T 中 U 对应的属性和类型,以此来创建一个新的对象类型。
当类型 T 中不存在要挑选的属性会报错
Omit
function toArray(a, b) { return [a, b] }8
与Pick的功能是互补的,挑选出对象类型 T 中不在 U 中的属性和类型,以此来创建一个新的对象类型。
当类型 T 中不存在要挑选的属性不会报错
Exclude
function toArray(a, b) { return [a, b] }9
从类型 T 中剔除所有可以赋值给类型 U 的类型。
当把类型 T 的所有类型都剔除光了,会得到 never
类型。下面代码中字面量类型 a
b
c
都是 string
的子类型(都可以赋值给 string
类型),于是 C
是 never
类型。
Extract
function toArray(a: number, b: number): number[] { return [a, b] }0
与Exclude的功能是互补的,从类型 T 中获取所有可以赋值给类型 U 的类型。
泛型常见错误
function toArray(a: number, b: number): number[] { return [a, b] }1
这里泛型参数 T 的约束为 Point
类型,函数的泛型定义表达的意思是:
参数
x
和返回值类型相同参数
x
和返回值都满足类型约束
根据[[鸭式辨型]],我们知道返回值 { x: 0, y: 0 }
满足类型约束 T extends Point
,但是它与 Point
类型并不相同,故不满足上述第 2 点
这里已经知道返回值类型满足类型约束,可以使用[[类型断言]]将返回值断言成 T 类型解决这个类型报错