Skip to content函数签名中的
TypeScript 泛型概述
编写可复用的函数时,既要保持通用性,又希望保留准确的类型信息。为具体的类型编写函数签名很直接,可一旦要求一个函数处理多种类型,最初想到的写法往往会让类型信息丢失。
遗失的类型信息:any 与 unknown 的尝试
从一个最简单的“原样返回”函数开始:
typescript
function identity(value: number): number {
return value;
}这个函数只能处理 number。如果需要它也能接收字符串、对象或数组,自然会考虑放宽参数的类型限制。
any 导致类型信息丢失
把参数和返回值都标注为 any 是最直接的做法[1]:
typescript
function identity(value: any): any {
return value;
}这样一来函数可以接收任意值,但返回值的类型与输入之间不再有任何联系——得到的结果永远是 any。
typescript
const result = identity("hello");
// result 的类型是 any,不是 string
result.toUpperCase(); // 编辑器不会提示,运行时却可能出错any 方便的背后是类型检查的退出。用它编写的通用函数,在那一段代码里基本上回到了 JavaScript 的动态类型。
unknown 的安全性与收窄成本
any 会关闭类型检查,于是很容易想到改用 unknown。unknown 和 any 一样可以接受任何值,但要求在使用值之前先做类型检查[1]。
typescript
function identity(value: unknown): unknown {
return value;
}
const result = identity("hello");
// result 类型为 unknown
result.toUpperCase();
// ~~~~~~~~~~~ 类型错误:对象的类型为 "unknown"要调用 toUpperCase,必须先做类型收窄:
typescript
const result = identity("hello");
if (typeof result === "string") {
console.log(result.toUpperCase()); // 这里才安全
}unknown 保证了安全性,但把类型检查的负担转移给了调用方。调用方必须知道自己传入的是什么、返回的是什么,然后手动收窄——每次调用都要写一次。只是一个简单的 identity 函数,代价已经不小;对于数组映射、对象合并这类实际的通用函数,收窄就更繁琐了。
根本问题在于:any 和 unknown 都无法在函数签名中表达“输入的类型与输出的类型是同一个”这一层关系。
引入泛型:用类型参数留住类型关系
泛型用来解决这种“保留类型关系”的问题。它允许在定义函数时不固定具体类型,而是声明一个类型参数,调用时再填入实际类型,同时保持输入与输出的关联。
用泛型重写 identity 函数:
typescript
function identity<T>(value: T): T {
return value;
}这里的 <T> 声明了一个类型参数 T。参数 value 的类型是 T,返回值类型也是 T。这个签名明确表达出:无论 T 是什么,返回值类型一定和参数一致。
调用时 TypeScript 会跟踪这个关系:
typescript
const s = identity("hello");
// s 的类型被推断为 string,不再是 any 或 unknown
s.toUpperCase(); // 类型检查正常工作
const n = identity(42);
// n 的类型是 number
n.toFixed(2);类型信息没有丢失,调用方也无需手动收窄——泛型把类型关系写在了签名里。
泛型函数的声明与调用
函数签名中的 <T> 声明
类型参数写在函数名后面的尖括号里,例如 function fn<T>(…)。T 只是一个占位符,可以用任意合法的标识符,习惯上会使用 T(Type)、U、K(Key)、V(Value)等简短的名字。
类型参数声明之后就可以在参数列表和返回值类型中使用,也可以同时用在多个位置,以此建立不同参数之间、参数与返回值之间的类型约束。
一个稍复杂的例子:接收两个值,返回由它们组成的二元数组。
typescript
function makeTuple<T>(a: T, b: T): [T, T] {
return [a, b];
}这里两个参数都用了 T,TypeScript 会要求它们类型一致,返回值也同样是那个类型的元组。
显式指定类型参数调用
调用泛型函数时可以显式写出类型参数:
typescript
const tuple = makeTuple<string>("a", "b");
// tuple 类型为 [string, string]当类型推断不足以确定类型参数时(例如参数本身是空数组或 null),或者需要强制将函数调用限定在某个类型上,这种写法就会派上用场。
依赖类型推断调用
大多数时候并不需要显式写 <T>。TypeScript 会依据传入的实参自动推断出 T 的类型:
typescript
const tuple = makeTuple(1, 2);
// T 被推断为 number只有在推断不出来时才需要显式指定。编写泛型代码时优先靠推断,让调用方保持简洁。
类型推断:让 TypeScript 自动确定 T
推断过程大致为:调用 makeTuple(1, 2) 时,TypeScript 检查实参类型,发现两个值都是 number;然后对照签名 makeTuple<T>(a: T, b: T),将 T 推到 number,最终把返回值类型计算为 [number, number]。
推断会综合所有实参的信息来求解类型参数。如果实参给出的信息彼此不一致,编译器会直接报错,指出类型不兼容。
typescript
makeTuple(1, "hello");
// ~~~~~~~ 类型 string 不能赋给类型 number这意味着泛型不仅在运行时给出正确结果,还在编写阶段就帮我们检查到误用。
类型推断也常常结合上下文。比如将泛型函数的返回值赋给一个已有明确类型的变量时,类型参数也可能受该变量类型的约束。不过在日常调用中,最主要的推断来源仍然是函数的参数。
场景对比:any、unknown 与泛型
这三个机制并不是互相替代的关系,各有各的用途。
- any:当一段代码确实无法确定类型,或正在与未标注类型的 JavaScript 模块交互时,
any能快速关闭检查,代价是失去该分支下游的所有类型保障。 - unknown:当需要表示“可以是任何值,但类型未知”,并且必须由使用方自己确认类型时使用。常见于用户输入、API 响应体等边界场景。与
any不同,unknown仍然维持了类型系统的完整性,只是要求使用端做收窄。 - 泛型:当函数或数据结构需要保持多种类型之间的关联时使用。它不是“忘记类型”,而是“推迟类型的确定”。泛型保证了在不同调用中类型参数可以不同,但在单次调用内部,类型关系是确定的。
举例来说:
- 日志函数
log(msg: any)可以接受任意消息,因为它只负责输出,不关心结构。用any避免了多余的泛型声明。 - JSON 解析函数
parse(json: string): unknown返回unknown,把进一步确认结构的责任交给调用方,比返回any更安全。 - 工具函数
map<T, U>(arr: T[], fn: (x: T) => U): U[]必须保留输入数组元素类型T与输出元素类型U的关系,所以采用泛型。
在函数复用场景里,只要输入和输出的类型是关联的,就应当优先考虑泛型。不要将 any 当做“通用函数”的起点——它只是特殊情形下的逃生出口。
