泛型约束
本教程共 80 篇 · 第 39 篇 · 更新于 2026-08-10 · 约 10 分钟阅读
本节目标:学会用
extends给泛型参数设置边界,避免在泛型函数体中访问不存在的属性;掌握keyof与泛型组合的模式和泛型默认值。
上一章我们写了 first<T> 这样来者不拒的泛型函数。但实际开发中,你经常需要泛型参数满足某种条件——比如”这个 T 必须有 length 属性”或者”这个 K 必须是 T 的某个键名”。泛型约束就是用 extends 关键字把这个条件写出来。
为什么需要约束
先看一个会出错的例子:
function getLength<T>(arg: T): number {
return arg.length;
// ❌ 类型 "T" 上不存在属性 "length"
}
编译器不认识 arg.length,因为 T 可能是 number,而 number 没有 length 属性。你需要告诉 TypeScript:T 不是随便什么类型,它必须拥有 .length。
extends 约束:为泛型参数设下限
语法很简单——在类型参数后面加 extends 类型:
interface HasLength {
length: number;
}
function getLength<T extends HasLength>(arg: T): number {
return arg.length; // ✅ 没问题了
}
getLength("hello"); // ✅ string 有 length
getLength([1, 2, 3]); // ✅ 数组有 length
getLength({ length: 10 }); // ✅ 只要满足接口就行
// getLength(123); // ❌ number 没有 length
T extends HasLength 的意思是:T 必须兼容 HasLength——也就是说,传入的类型至少要有 length: number 这个属性。这是泛型约束最基础的用法。
约束的实际价值
约束不只是”让编译器闭嘴”,它还让你能在函数体内安全地使用被约束的属性:
function logAndReturn<T extends { toString(): string }>(value: T): T {
console.log(value.toString()); // 安全——T 一定有 toString
return value;
}
logAndReturn(42); // 输出 "42"
logAndReturn(new Date()); // 输出当前时间字符串
// logAndReturn(null); // ❌ null 没有 toString
keyof 与泛型:精准锁定对象键名
约束不止能限制”这个类型有没有某个属性”,还能限制”这个类型参数必须是另一个类型的键”。
keyof 快速回顾
keyof T 返回 T 所有键名组成的联合类型:
interface User {
id: number;
name: string;
age: number;
}
type UserKey = keyof User;
// type UserKey = "id" | "name" | "age"
典型模式:getProperty
把 keyof 和泛型约束组合,就能写出类型安全的属性访问函数:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = { id: 1, name: "张三", age: 25 };
const name = getProperty(user, "name"); // ✅ name 类型是 string
// const foo = getProperty(user, "foo"); // ❌ "foo" 不是 User 的键
这里的魔法是 K extends keyof T。它告诉 TypeScript:K 只能是 T 的键名之一。于是:
- 传
"foo"会直接报错,因为"foo"不在"id" | "name" | "age"中。 - 传
"name"时,返回值类型自动推导为T["name"],也就是string。
这是 TypeScript 里非常经典的模式——几乎每一个写泛型工具库的人都会用到。
多泛型参数相互约束
类型参数不只能受”外部类型”约束,还可以受另一个类型参数约束:
// U 必须是 T 的属性之一
function assign<T, U extends keyof T>(target: T, source: Pick<T, U>): T {
return { ...target, ...source };
}
const user: User = { id: 1, name: "张三", age: 25 };
// ✅ 只更新 name 和 age
assign(user, { name: "李四", age: 26 });
// ❌ "email" 不是 User 的属性
// assign(user, { email: "test@test.com" });
这里的 U extends keyof T 让第二个参数的类型精确限制到”只能是 T 的部分属性”,不会出现随便传一堆不存在的属性的情况。
泛型参数的默认类型
跟函数的参数默认值类似,泛型参数也可以有默认类型。当你调用时没有显式指定类型参数,且编译器推断不出来,就会用默认值:
// T 默认是 string
function createContainer<T = string>(): { value: T | null } {
return { value: null };
}
// 没指定 T,默认用 string
const container1 = createContainer();
// container1: { value: string | null }
// 显式指定
const container2 = createContainer<number>();
// container2: { value: number | null }
默认类型必须满足约束条件(如果有约束的话):
interface HasLength {
length: number;
}
// 默认值 string 满足 HasLength(string 有 length)
function process<T extends HasLength = string>(value: T): T {
return value;
}
默认类型的规则
- 有默认值的类型参数被视为可选。
- 必选的类型参数不能在可选类型参数之后。
- 当同时指定类型参数时,只需要为必选参数指定;未指定的可选参数会自动用默认值。
// T 必选,U 可选(有默认值 string)
function pair<T, U = string>(first: T, second: U): [T, U] {
return [first, second];
}
pair(1, "hello"); // T=number, U=string(推断)
pair<number>(1, "hi"); // T=number, U=string(默认值)
pair<number, boolean>(1, true); // T=number, U=boolean(显式指定)
小结
泛型约束用 extends 三个场景覆盖了大部分实际需求:
- 限制类型参数必须有某个属性或形状——
T extends { length: number } - 限制类型参数必须是另一个类型的键——
K extends keyof T - 类型参数之间相互约束——
U extends keyof T
再加一个默认类型 T = SomeType,泛型就同时具备了灵活性和安全边界。下一章我们看看类级别和接口级别的泛型约束能做什么。