Menu

TypeScript 泛型:泛型函数、泛型类型和泛型类

泛型让函数、接口、类型或类能处理多种类型,同时让这些类型保持关联:输入是什么,决定了输出是什么。学习泛型函数、类型参数推断、多个类型参数、泛型接口和泛型类、默认值,以及什么时候不该用泛型。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

泛型就是类型参数。泛型函数声明一个占位类型,通常叫 T,每次调用时都会把它填上,所以输入的类型决定了输出的类型。这样一个函数就能同时处理数字、字符串和你自己的对象,而且每次调用依然有完整的类型。

函数名后面的 <T> 声明了类型参数。items: T[] 把它用在输入上,T | undefined 把它用在输出上,从而把两者关联起来。你从来没写过 number 或 string:TypeScript 根据实参推断出了 T。

为什么不用 any 或 unknown?

没有泛型的话,你会把参数类型写成 any 或 unknown。两者都能接受任何数组,但都丢掉了输入和输出之间的关联:

参数类型接受任何输入结果类型是否检查
any是any否:错误也能通过编译
unknown是unknown是,但使用前必须收窄
T(泛型)是与输入关联是,而且类型精确

类型参数推断

通常 TypeScript 会根据你传入的值推断类型实参。你也可以在调用时用尖括号显式写出来;当实参中没有任何东西涉及 T,或者推断的结果不是你想要的时候,就需要这样做。

有两个推断结果值得了解。不显式写类型实参的 pair(1, "one") 会报错:TypeScript 根据第一个实参推断出 T = number,然后报告 Argument of type 'string' is not assignable to parameter of type 'number'.(TS2345)。而完全没有实参的 emptyList() 会得到 T = unknown,也就是 unknown[],这很少是你想要的。

多个类型参数

一个函数可以按需声明任意多个类型参数,用逗号分隔。每个参数都会被独立推断。

mapValues 一次推断出三样东西:K 是 "tea" | "cake",V 是 number,R 根据回调的返回值推断为 string。K extends string 是一个约束,下一页会讲到。

泛型接口和类型别名

类型也可以带参数。使用这个类型时要写上实参:Box<number>、ApiResponse<User>。大多数内置的集合类型都是这样的:Array<T>、Map<K, V>、Promise<T>、Record<K, V>。

Result<T, E = string> 还展示了默认类型参数:Result<number> 表示 Result<number, string>。和函数的可选参数一样,带默认值的参数必须放在必需的参数后面。

泛型类

类在名称后面接收类型参数,每个实例都会确定这些参数。字段、方法和构造函数的参数都可以使用它们。

如果构造函数接收一个 T,你就可以省略显式的类型实参:new Box(5) 会推断为 Box<number>。静态成员属于类本身而不是实例,所以不能使用类的类型参数(Static members cannot reference class type parameters.,TS2302)。

泛型箭头函数和函数类型

类型参数列表写在参数列表前面。同样的语法也用来描述泛型函数类型。

const last = <T>(items: T[]): T | undefined => items[items.length - 1];

type Mapper = <T, R>(items: T[], fn: (item: T) => R) => R[];
const mapAll: Mapper = (items, fn) => items.map(fn);

// In a .tsx file, <T> looks like a JSX tag. Add a trailing comma:
const lastTsx = <T,>(items: T[]) => items[items.length - 1];

用 const 类型参数推断字面量类型

默认情况下,泛型推断出的是放宽后的类型:pair("a", "b") 得到 [string, string]。当确切的值很重要时(路由名、事件名、列名列表),把参数标记为 const(TypeScript 5.0 及以上),它就会像实参写了 as const 一样进行推断:

调用者写的是普通数组,函数却保留了字面量类型。

什么时候不该用泛型

类型参数只有在关联两样东西时才有存在的价值:一个参数和返回类型、两个参数,或者一个参数和一个回调。如果 T 只出现一次,它就什么也没增加,直接写普通类型更清楚。

// Pointless: T is used once, so it is just a longer way to write unknown
function logValue<T>(value: T): void {
  console.log(value);
}

// Clearer
function logValueSimple(value: unknown): void {
  console.log(value);
}

// Also pointless: returns T but nothing connects T to an argument,
// so the caller is really just asserting a type
function parseJson<T>(text: string): T {
  return JSON.parse(text);
}

最后一种写法很常见,也很有误导性:parseJson<User>(text) 看起来类型安全,实际上什么都没检查,和 JSON.parse(text) as User 完全一样。应该返回 unknown 再进行校验。其他滥用的迹象还有:一个类型参数总是被传入同一个实参,或者它可以用两个已知类型的联合来代替。

如果泛型需要知道 T 的某些信息(比如它有 length,或者 K 是 T 的键),下一步就是用 extends 添加约束。

常见问题

TypeScript 中的泛型是什么?

泛型就是类型参数:像 T 这样的占位符,每次使用函数、接口、类型别名或类时,都会用一个真实的类型填进去。function first<T>(items: T[]): T | undefined 适用于任何数组,结果的类型就是你传入数组的元素类型,所以 first([1, 2]) 是 number | undefined,first(["a"]) 是 string | undefined。

泛型和 any 有什么区别?

any 会关闭类型检查:传入的值和返回的值之间没有任何关联,结果也是 any。泛型则保留了这种关联:identity<T>(x: T): T 返回的正是你传入的类型。如果你接受任何值、但不需要把类型传回去,就用 unknown;需要传回去,就用泛型。

TypeScript 中的 <T> 是什么意思?

它声明了一个名为 T 的类型参数。在 function wrap<T>(value: T) 中,T 是一个类型变量,每次调用时 TypeScript 都会根据实参把它填上,你也可以像 wrap<string>("a") 这样显式传入。T 这个名字只是惯例,TItem 或 Key 这样更长的名字效果完全一样。

TypeScript 中如何写泛型箭头函数?

把类型参数列表放在参数列表前面:const first = <T>(items: T[]): T | undefined => items[0];。在 .tsx 文件中,<T> 会被当成 JSX 标签,所以要写成带尾逗号的 <T,>,或者写成 <T extends unknown>。

TypeScript 中如何给泛型设置默认类型?

在参数后面加上 = Type:interface ApiResponse<T = unknown> { data: T }。这样不带类型实参的 ApiResponse 就表示 ApiResponse<unknown>。和函数的可选参数一样,带默认值的类型参数必须放在没有默认值的参数后面。

Coddy 编程语言插图

和 Coddy 一起学编程

开始