Skip to content

如果说 TypeScript 是一门对类型进行编程的语言,那么泛型就是这门语言里的(函数)参数。

类型别名中的泛型

类型别名如果声明了泛型坑位,那其实就等价于一个接受参数的函数:

typescript
type Factory<T> = T | number | string;

上面这个类型别名的本质就是一个函数,T 就是它的变量,返回值则是一个包含 T 的联合类型,我们可以写段伪代码来加深一下记忆:

typescript
function Factory(typeArg){
  return [typeArg, number, string]
}

工具类型

类型别名中的泛型大多是用来进行工具类型封装。

typescript
type Stringify<T> = {
  [K in keyof T]: string;
};

type Clone<T> = {
  [K in keyof T]: T[K];
};

Stringify 会将一个对象类型的所有属性类型置为 string ,而 Clone 则会进行类型的完全复制。 再比如, TypeScript 中的内置工具类型Partial实现

typescript
type Partial<T> = {
    [P in keyof T]?: T[P];
};

工具类型 Partial 会将传入的对象类型复制一份,但会额外添加一个,所有属性可选。

typescript
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)以及返回值(即:两端的值)。

typescript
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, 那么这个条件就为真

根据条件计算不同类型

typescript
// 将布尔类型转换为字符串类型
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

根据条件过滤联合类型

typescript
// 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

泛型默认值

像函数可以声明一个参数的默认值一样,泛型同样有着默认值的设定

typescript
type Factory<T = boolean> = T | number | string;

在调用时就可以不带任何参数了,默认会使用我们声明的默认值来填充

typescript
const foo: Factory = false;
typescript
// 伪代码
function Factory(typeArg = boolean){
  return [typeArg, number, string]
}

泛型约束

在泛型中,我们可以使用 extends 关键字来约束传入的泛型参数必须符合要求。关于 extends,A extends B 意味着 A 是 B 的子类型。 我们暂时只需要了解非常简单的判断逻辑,也就是说 A 比 B 的类型更精确,或者更复杂

  • 更精确:
    • 字面量类型是对应原始类型的子类型,即 'hello' extends string666 extends number 成立
    • 联合类型子集均为联合类型的子类型,即 11 | 2 1 | 2 | 3 | 4 的子类型。
  • 更复杂
    • 如 { name: string } 是 {} 的子类型,因为在 {} 的基础上增加了额外的类型,
    • 基类与派生类(父类与子类)同理
typescript
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 来标明类型约束. 如果我们想让这个类型别名可以无需显式传入泛型参数也能调用,并且默认情况下是成功地,这样就可以为这个泛型参数声明一个默认值:

typescript
type ResStatus<ResCode extends number = 10000> = ResCode extends 10000 | 10001 | 10002
  ? 'success'
  : 'failure';

type Res4 = ResStatus; // "success"

多泛型关联

typescript
type ProcessInput<
  Input,
  SecondInput extends Input = Input,
  ThirdInput extends Input = SecondInput
> = number;
  • 这个工具类型接受 1-3 个泛型参数。
  • 第二、三个泛型参数的类型需要是首个泛型参数的子类型
  • 当只传入一个泛型参数时,其第二个泛型参数会被赋值为此参数,而第三个则会赋值为第二个泛型参数,相当于均使用了这唯一传入的泛型参数
  • 当传入两个泛型参数时,第三个泛型参数会默认赋值为第二个泛型参数的值

泛型接口(对象)

我们经常需要定义带有泛型的接口,用于约束对象的结构。

typescript
// 定义一个泛型接口
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' };
typescript
// 定义一个泛型接口
interface GenericIdentity<T> {
  value: T;
  getIdentity: () => T;
}

// 创建一个对象使用泛型接口
let myIdentity: GenericIdentity<string> = {
  value: 'myString',
  getIdentity: () => 'myString'
}

由于泛型提供了对类型结构的复用能力,我们也经常在对象类型结构中使用泛型。最常见的一个例子应该还是响应类型结构的泛型处理:

typescript
interface IRes<TData = unknown> {
  code: number;
  error?: string;
  data: TData;
}

这个接口描述了一个通用的响应类型结构,预留出了实际响应数据的泛型坑位,然后在你的请求函数中就可以传入特定的响应类型了:

typescript
interface IUserProfileRes {
  name: string;
  homepage: string;
  avatar: string;
}

function fetchUserProfile(): Promise<IRes<IUserProfileRes>> {}

type StatusSucceed = boolean;
function handleOperation(): Promise<IRes<StatusSucceed>> {}

泛型嵌套的场景也非常常用,比如对存在分页结构的数据,我们也可以将其分页的响应结构抽离出来:

typescript
interface IPaginationRes<TItem = unknown> {
  data: TItem[];
  page: number;
  totalCount: number;
  hasNextPage: boolean;
}

function fetchUserProfileList(): Promise<IRes<IPaginationRes<IUserProfileRes>>> {}

泛型函数

  1. 泛型函数的定义

泛型函数是具有泛型参数的函数,它们可以接受不同类型的参数,从而增强函数的灵活性和复用性。

  1. 泛型函数的使用

在泛型函数中,可以使用泛型参数来定义参数类型返回值类型函数体内的变量类型,以增加函数的通用性。例如:

typescript
// 定义一个泛型函数  T定义了参数类型和返回值类型
function identity<T>(arg: T): T {
  return arg;
}

// 使用泛型函数
const output1 = identity<string>('myString');
const output2 = identity<number>(100);
typescript

function handle<T>(input: T): T {}
  1. 类型推断

在调用泛型函数时,可以省略泛型参数的显式指定,TypeScript 可以根据传入的参数类型自动推断出泛型参数的类型。


在基于参数类型进行填充泛型时,其类型信息会被推断到尽可能精确的程度,如这里会推导到字面量类型而不是基础类型。这是因为在直接传入一个值时,这个值是不会再被修改的,因此可以推导到最精确的程度。而如果你使用一个变量作为参数,那么只会使用这个变量标注的类型(在没有标注时,会使用推导出的类型)。

typescript
function handle<T>(input: T): T {}

const author = 'yyds' // 使用 const 声明,被推导为 "yyds"

let authorAge = 18 // 使用 let 声明,被推导为 number

handle(author) // 填充为字面量类型 "yyds"
handle(authorAge) // 填充为基础类型 number
typescript
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 是一个泛型函数,它可以接受不同类型的参数,并返回相同类型的值。通过指定不同的类型参数,可以创建具有不同返回值的实例。


  1. 泛型约束

有时候我们希望泛型参数满足一定的条件,这时可以使用泛型约束来限制泛型参数的类型。例如,**<T extends SomeType>**,表示 T 必须是 SomeType 或其子类型。

typescript
// 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];
}
typescript
// 对于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>;
    });
}

泛型类

泛型类是具有泛型参数的类,它们可以接受不同类型的参数,从而增强代码的灵活性和复用性

  1. 泛型类的定义

泛型类的定义方式与普通类类似,只是在类名后面加上尖括号,里面放置类型参数。 例如:

typescript
class GenericClass<T> {...}
  1. 泛型参数的使用

在泛型类中,可以在类的成员函数、成员变量以及构造函数中使用泛型参数,以增加类的通用性。

  1. 泛型类的实例化

在实例化泛型类时,需要指定具体的泛型参数。

typescript
const myInstance = new MyGenericClass<number>()
  1. 泛型约束

有时候我们希望泛型参数满足一定的条件,这时可以使用泛型约束来限制泛型参数的类型。例如,**<T extends SomeType>**,表示 T 必须是 SomeType 或其子类型。

typescript
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'
typescript
// 定义一个泛型类
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; }
typescript
// 定义一个泛型类
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等

typescript
interface Todo {
  title: string;
  description: string;
}

// Partial 让所有属性都变成可选
type TodoPreview = Partial<Todo>;
// {title?: string, description?: string}

// Required 让所有属性都变成必选
type TodoRequired = Required<Todo>;
// {title: string, description: string}