泛型函数
本教程共 80 篇 · 第 22 篇 · 更新于 2026-08-10 · 约 12 分钟阅读
本节目标:学会用泛型写出能处理多种类型但不丢失类型信息的函数,掌握类型参数推断、泛型约束和多泛型参数。
前面我们写的函数,参数和返回值类型都是固定的。但有时你需要一个函数”能处理多种类型”,而且”输入什么类型,输出就是什么类型”。any 可以做到前者,但做不到后者——它会丢掉类型信息。泛型就是来解决这个问题的。
类型参数:用 <T> 关联输入输出
先看一个最简单的例子——identity 函数(直接返回传入的参数):
// ❌ 用 any——丢失了类型信息
function identity(arg: any): any {
return arg;
}
const result = identity("hello");
// result 的类型是 any,不是 string
// ✅ 用泛型——保留了类型信息
function identity<T>(arg: T): T {
return arg;
}
const result = identity("hello");
// result 的类型是 string ✅
const num = identity(42);
// num 的类型是 number ✅
<T> 就是一个类型参数——它像一个类型变量,TypeScript 会根据你传入的实参自动推断 T 是什么。(arg: T): T 表达了一个关系:输入是 T,输出也是 T,两者绑定在一起。
泛型的核心思想就是这个:建立类型之间的对应关系。输入类型和输出类型不再独立,而是通过类型参数关联起来。
类型参数推断
大多数时候,你不需要手动指定类型参数——TypeScript 会从实参推断:
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const s = firstElement(["a", "b", "c"]); // s: string | undefined
const n = firstElement([1, 2, 3]); // n: number | undefined
你看,调用 firstElement(["a", "b", "c"]) 时,TypeScript 从数组内容推断出 T = string,所以返回值是 string | undefined。整个过程你一句话没说。
但有些场景推断不出来,你就需要显式指定:
function combine<T>(arr1: T[], arr2: T[]): T[] {
return arr1.concat(arr2);
}
// ❌ 推断失败:arr1 是 number[],arr2 是 string[],T 该是 number 还是 string?
combine([1, 2], ["a", "b"]); // 报错
// ✅ 显式指定 T 为联合类型
const result = combine<string | number>([1, 2], ["a", "b"]);
// result: (string | number)[]
Tip类型参数的名字习惯用单个大写字母,如
T(Type)、U、V。你也可以用更描述性的名字,比如TItem、TKey——在复杂场景下可读性更好。
泛型约束:用 extends 限制类型
泛型参数默认可以接受任意类型,所以函数体中你不能随便用 T 的属性:
function logLength<T>(arg: T): T {
console.log(arg.length);
// ~~~~~~~~ ❌ 类型 "T" 上不存在属性 "length"
return arg;
}
报错是因为 T 可以是任何类型,比如 number 就没有 .length。用 extends 加约束就能解决:
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length); // ✅ T 一定有 length 属性
return arg;
}
logLength("hello"); // ✅ string 有 length
logLength([1, 2, 3]); // ✅ 数组有 length
logLength({ length: 10 }); // ✅ 对象也有 length
logLength(42); // ❌ number 没有 length
T extends HasLength 的意思是:T 必须是 HasLength 的子类型——也就是说,传入的任何类型都必须有 length: number 这个属性。
一个常见的约束用法是配合 keyof:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: "Alice", age: 30 };
getProperty(person, "name"); // ✅ 返回 string,"Alice"
getProperty(person, "age"); // ✅ 返回 number,30
getProperty(person, "email"); // ❌ "email" 不在 "name" | "age" 中
K extends keyof T 保证了第二个参数 key 只能是 T 的已有属性名——拼写错误会在编译期被拦下。
常见泛型模式
几个你在日常开发中最可能遇到的泛型模式:
identity(原样返回)
function identity<T>(value: T): T {
return value;
}
最简单的泛型,作用是”不丢失类型地传递一个值”。
数组 filter(保留元素类型)
function filterArray<T>(arr: T[], predicate: (item: T) => boolean): T[] {
return arr.filter(predicate);
}
const numbers = [1, 2, 3, 4, 5];
const even = filterArray(numbers, (n) => n % 2 === 0);
// even: number[]——类型被保留了
map(输入和输出类型可以不同)
function mapArray<T, U>(arr: T[], transform: (item: T) => U): U[] {
return arr.map(transform);
}
const strings = ["1", "2", "3"];
const parsed = mapArray(strings, (s) => parseInt(s));
// parsed: number[] ✅
这里用了两个类型参数——T 是输入数组的元素类型,U 是输出数组的元素类型。它们可以不同。
比较(泛型约束)
function longer<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
const longerArr = longer([1, 2], [1, 2, 3]); // [1, 2, 3](number[])
const longerStr = longer("hi", "hello"); // "hello"("hi" | "hello")
longer 的返回类型跟传入的类型一致——传入 string[] 就返回 string[],传入两个字面量字符串就返回它们的联合类型。
写泛型函数的三个准则
泛型容易上瘾,但过度使用反而让代码更难读。记住三条准则:
1. 类型参数应该出现至少两次
如果一个类型参数只出现了一次,它没在关联任何东西——可能你根本不需要泛型。
// ❌ Str 只用了一次——不需要泛型
function greet<Str extends string>(s: Str) {
console.log("Hello, " + s);
}
// ✅ 直接用 string
function greet(s: string) {
console.log("Hello, " + s);
}
2. 尽量少用类型参数
多一个类型参数就多一层复杂度。尤其不要为无关的东西创建类型参数。
// ❌ Func 没有关联两个值——多余的泛型参数
function filterArr<T, Func extends (arg: T) => boolean>(
arr: T[],
func: Func
): T[] {
return arr.filter(func);
}
// ✅ Func 不需要单独的类型参数
function filterArr<T>(arr: T[], func: (arg: T) => boolean): T[] {
return arr.filter(func);
}
3. 能用类型参数本身,就别用约束
// ❌ 不必要地约束了 T
function firstEl<T extends any[]>(arr: T) {
return arr[0];
}
// 返回值类型是 any
// ✅ 直接让 T 代表元素类型
function firstEl<T>(arr: T[]) {
return arr[0];
}
// 返回值类型是 T
第二种写法不仅更简洁,返回类型也更精确(T 而不是 any)。
小结
泛型函数的核心是用类型参数建立输入输出之间的类型关系。<T> 定义类型参数,TypeScript 自动推断,extends 加约束。常见模式有 identity、array filter/map、以及带约束的比较函数。记住三准则:类型参数出现至少两次、越少越好、能用类型参数就别约束。