如果说 TypeScript 是一门对类型进行编程的语言,那么泛型就是这门语言里的(函数)参数。
类型别名中的泛型
类型别名如果声明了泛型坑位,那其实就等价于一个接受参数的函数:
type Factory<T> = T | number | string;
上面这个类型别名的本质就是一个函数,T 就是它的变量,返回值则是一个包含 T 的联合类型,我们可以写段伪代码来加深一下记忆:
function Factory(typeArg){
return [typeArg, number, string]
}
工具类型
类型别名中的泛型大多是用来进行工具类型封装。
type Stringify<T> = {
[K in keyof T]: string;
};
type Clone<T> = {
[K in keyof T]: T[K];
};
Stringify 会将一个对象类型的所有属性类型置为 string ,而 Clone 则会进行类型的完全复制。 再比如, TypeScript 中的内置工具类型Partial
实现
type Partial<T> = {
[P in keyof T]?: T[P];
};
工具类型 Partial 会将传入的对象类型复制一份,但会额外添加一个?
,所有属性可选。
interface IFoo {
prop1: string;
prop2: number;
prop3: boolean;
prop4: () => void;
}
type PartialIFoo = Partial<IFoo>;
// 等价于
interface PartialIFoo {
prop1?: string;
prop2?: number;
prop3?: boolean;
prop4?: () => void;
}
条件类型
在条件类型参与的情况下,通常泛型会被作为条件类型中的判断条件(T extends Condition
,或者 Type extends T
)以及返回值(即:
两端的值)。
type IsEqual<T> = T extends true ? 1 : 2;
type A = IsEqual<true>; // 1
type B = IsEqual<false>; // 2
type C = IsEqual<'666'>; // 2
这里的 IsEqual 是一个条件类型,它检查传入的类型参数 T 是否能被赋值给 true。
- 如果 T 是 true,那么结果类型是 1
- 如果 T 不是 true,那么结果类型是 2
我们需要理解 extends 关键字在条件类型中的作用。它不仅用于继承接口,也可以用于类型关系检查。A extends B 的含义是:如果类型 A 可以满足约束条件 B, 那么这个条件就为真。
根据条件计算不同类型
// 将布尔类型转换为字符串类型
type BooleanToNumber<T extends boolean> = `${T}`;
type TrueToOne<T extends true> = 1; // 如果 T 是 true,返回 1
type FalseToZero<T extends false> = 0; // 如果 T 是 false,返回 0
type A = BooleanToNumber<true>; // '1'
type B = TrueToOne<true>; // 1
type C = FalseToZero<false>; // 0
根据条件过滤联合类型
// KeepTrue 是一个条件类型,它会过滤掉联合类型中不为 true 的部分
type KeepTrue<T> = T extends true ? T : never;
// 泛型T并行 接收三个参数 自动分发为
// KeepTrue<true> ; KeepTrue<false> ; KeepTrue<true>
type TrueValues = KeepTrue<true | false | true>; // true | true
type A = TrueValues; // true
泛型默认值
像函数可以声明一个参数的默认值一样,泛型同样有着默认值的设定
type Factory<T = boolean> = T | number | string;
在调用时就可以不带任何参数了,默认会使用我们声明的默认值来填充
const foo: Factory = false;
// 伪代码
function Factory(typeArg = boolean){
return [typeArg, number, string]
}
泛型约束
在泛型中,我们可以使用 extends 关键字来约束传入的泛型参数必须符合要求。关于 extends,A extends B 意味着 A 是 B 的子类型。 我们暂时只需要了解非常简单的判断逻辑,也就是说 A 比 B 的类型更精确,或者更复杂
- 更精确:
- 字面量类型是对应原始类型的子类型,即
'hello' extends string
,666 extends number
成立 - 联合类型子集均为联合类型的子类型,即
1
、1 | 2
是1 | 2 | 3 | 4
的子类型。
- 字面量类型是对应原始类型的子类型,即
- 更复杂
- 如 { name: string } 是 {} 的子类型,因为在 {} 的基础上增加了额外的类型,
- 基类与派生类(父类与子类)同理
type ResStatus<ResCode extends number> = ResCode extends 10000 | 10001 | 10002
? 'success'
: 'failure';
type Res1 = ResStatus<10000>; // "success"
type Res2 = ResStatus<20000>; // "failure"
type Res3 = ResStatus<'10000'>; // 类型“string”不满足约束“number”。
通过 extends number 来标明类型约束. 如果我们想让这个类型别名可以无需显式传入泛型参数也能调用,并且默认情况下是成功地,这样就可以为这个泛型参数声明一个默认值:
type ResStatus<ResCode extends number = 10000> = ResCode extends 10000 | 10001 | 10002
? 'success'
: 'failure';
type Res4 = ResStatus; // "success"
多泛型关联
type ProcessInput<
Input,
SecondInput extends Input = Input,
ThirdInput extends Input = SecondInput
> = number;
- 这个工具类型接受 1-3 个泛型参数。
- 第二、三个泛型参数的类型需要是首个泛型参数的子类型。
- 当只传入一个泛型参数时,其第二个泛型参数会被赋值为此参数,而第三个则会赋值为第二个泛型参数,相当于均使用了这唯一传入的泛型参数。
- 当传入两个泛型参数时,第三个泛型参数会默认赋值为第二个泛型参数的值。
泛型接口(对象)
我们经常需要定义带有泛型的接口,用于约束对象的结构。
// 定义一个泛型接口
interface KeyValue<T, U> {
key: T;
value: U;
}
// 使用泛型接口
const obj1: KeyValue<string, number> = { key: 'foo', value: 123 };
const obj2: KeyValue<number, string> = { key: 456, value: 'bar' };
// 定义一个泛型接口
interface GenericIdentity<T> {
value: T;
getIdentity: () => T;
}
// 创建一个对象使用泛型接口
let myIdentity: GenericIdentity<string> = {
value: 'myString',
getIdentity: () => 'myString'
}
由于泛型提供了对类型结构的复用能力,我们也经常在对象类型结构中使用泛型。最常见的一个例子应该还是响应类型结构的泛型处理:
interface IRes<TData = unknown> {
code: number;
error?: string;
data: TData;
}
这个接口描述了一个通用的响应类型结构,预留出了实际响应数据的泛型坑位,然后在你的请求函数中就可以传入特定的响应类型了:
interface IUserProfileRes {
name: string;
homepage: string;
avatar: string;
}
function fetchUserProfile(): Promise<IRes<IUserProfileRes>> {}
type StatusSucceed = boolean;
function handleOperation(): Promise<IRes<StatusSucceed>> {}
泛型嵌套的场景也非常常用,比如对存在分页结构的数据,我们也可以将其分页的响应结构抽离出来:
interface IPaginationRes<TItem = unknown> {
data: TItem[];
page: number;
totalCount: number;
hasNextPage: boolean;
}
function fetchUserProfileList(): Promise<IRes<IPaginationRes<IUserProfileRes>>> {}
泛型函数
- 泛型函数的定义
泛型函数是具有泛型参数的函数,它们可以接受不同类型的参数,从而增强函数的灵活性和复用性。
- 泛型函数的使用
在泛型函数中,可以使用泛型参数来定义参数类型、返回值类型或函数体内的变量类型,以增加函数的通用性。例如:
// 定义一个泛型函数 T定义了参数类型和返回值类型
function identity<T>(arg: T): T {
return arg;
}
// 使用泛型函数
const output1 = identity<string>('myString');
const output2 = identity<number>(100);
function handle<T>(input: T): T {}
- 类型推断
在调用泛型函数时,可以省略泛型参数的显式指定,TypeScript 可以根据传入的参数类型自动推断出泛型参数的类型。
在基于参数类型进行填充泛型时,其类型信息会被推断到尽可能精确的程度,如这里会推导到字面量类型而不是基础类型。这是因为在直接传入一个值时,这个值是不会再被修改的,因此可以推导到最精确的程度。而如果你使用一个变量作为参数,那么只会使用这个变量标注的类型(在没有标注时,会使用推导出的类型)。
function handle<T>(input: T): T {}
const author = 'yyds' // 使用 const 声明,被推导为 "yyds"
let authorAge = 18 // 使用 let 声明,被推导为 number
handle(author) // 填充为字面量类型 "yyds"
handle(authorAge) // 填充为基础类型 number
function identity<T>(arg: T): T {
return arg;
}
// 使用泛型函数
let result1 = identity<string>('Hello');
console.log(result1); // 输出: 'Hello'
let result2 = identity<number>(42);
console.log(result2); // 输出: 42
// 类型推断:可以省略泛型参数的显式指定
let result3 = identity(true); // TypeScript 可以自动推断出参数类型为 boolean
console.log(result3); // 输出: true
identity 是一个泛型函数,它可以接受不同类型的参数,并返回相同类型的值。通过指定不同的类型参数,可以创建具有不同返回值的实例。
- 泛型约束
有时候我们希望泛型参数满足一定的条件,这时可以使用泛型约束来限制泛型参数的类型。例如,**<T extends SomeType>**
,表示 T 必须是 SomeType 或其子类型。
// swap泛型函数接收两个参数,start, end,
// ([start, end]: [T, U]) start解构于T,end解构于U
function swap<T extends number, U extends number>([start, end]: [T, U]): [U, T] {
return [end, start];
}
// 对于fetchData<T>, 泛型参数T的具体类型由调用fetchData函数传入的类型决定
// 但这里没有显示指定,TS可以根据Promise<T>做推断
// Promise<T> 表示 Promise对象值的类型是T;
// 即如果response.json()返回的是string,T就是string
// 如果返回number, T就是number
function fetchData<T>(url: string): Promise<T> {
return fetch(url).then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
});
}
泛型类
泛型类是具有泛型参数的类,它们可以接受不同类型的参数,从而增强代码的灵活性和复用性
- 泛型类的定义
泛型类的定义方式与普通类类似,只是在类名后面加上尖括号,里面放置类型参数。 例如:
class GenericClass<T> {...}
- 泛型参数的使用
在泛型类中,可以在类的成员函数、成员变量以及构造函数中使用泛型参数,以增加类的通用性。
- 泛型类的实例化
在实例化泛型类时,需要指定具体的泛型参数。
const myInstance = new MyGenericClass<number>()
- 泛型约束
有时候我们希望泛型参数满足一定的条件,这时可以使用泛型约束来限制泛型参数的类型。例如,**<T extends SomeType>**
,表示 T 必须是 SomeType 或其子类型。
class Box<T> {
private item: T;
constructor(item: T) {
this.item = item;
}
getItem(): T {
return this.item;
}
}
// 使用泛型类
const numberBox = new Box<number>(42);
console.log(numberBox.getItem()); // 输出: 42
const stringBox = new Box<string>('Hello');
console.log(stringBox.getItem()); // 输出: 'Hello'
// 定义一个泛型类
class GenericNumber<T> {
value: T;
add: (x: T, y: T) => T;
}
// 实例化泛型类
let numberObj = new GenericNumber<number>();
numberObj.value = 0;
numberObj.add = function(x, y) { return x + y; }
// 定义一个泛型类
class Queue<T> {
private data: T[] = [];
enqueue(item: T) {
this.data.push(item);
}
dequeue(): T | undefined {
return this.data.shift();
}
}
// 使用泛型类
const numberQueue = new Queue<number>();
numberQueue.enqueue(1);
numberQueue.enqueue(2);
console.log(numberQueue.dequeue()); // 1
const stringQueue = new Queue<string>();
stringQueue.enqueue('a');
stringQueue.enqueue('b');
console.log(stringQueue.dequeue()); // 'a'
泛型工具类型
TypeScript提供了一些内置的泛型工具类型,如Partial、Required、Readonly等
interface Todo {
title: string;
description: string;
}
// Partial 让所有属性都变成可选
type TodoPreview = Partial<Todo>;
// {title?: string, description?: string}
// Required 让所有属性都变成必选
type TodoRequired = Required<Todo>;
// {title: string, description: string}