讲解

裸泛型 T 表示「任意类型」,函数内部对它几乎什么都不能做。泛型约束用 extends 限定 T 的最低要求:function longest<T extends { length: number }>(a: T, b: T) 表示「T 是什么都行,但必须有 length 属性」。约束之后,函数体里就能安全使用约束声明的能力,调用方传入不满足约束的类型则编译报错。注意 T extends X 里的 extends 读作「是 X 的子类型」,和类的继承不是一回事(虽然语法相同)。

第二个进阶工具是默认类型参数:<T = string> 表示调用方不指定、也无法推断时,T 取 string。React 的 Component<P = {}>、很多库的 options 泛型都靠默认值让「简单调用零负担,复杂调用可配置」。

第三个常用模式是「用另一个参数约束这个参数」:function get<T, K extends keyof T>(obj: T, key: K) 表示 K 必须是 T 的键——obj 传什么,key 就只能填它的属性名,返回值还能精确到对应属性的类型 T[K]。keyof 下一章详细展开,这里先体会「泛型之间相互约束」的表达力,这是类型安全工具函数(如 lodash 的 get、表单的 setFieldValue)的标准写法。

示例

泛型约束:要求必须有 length:

function logLength<T extends { length: number }>(value: T): T {
  console.log(`长度:${value.length}`);
  return value;
}

logLength("hello"); // string 有 length
logLength([1, 2, 3]); // 数组有 length
// logLength(42); // 若取消注释:number 没有 length,编译报错

默认类型参数:

interface Paged<T = unknown> {
  page: number;
  size: number;
  items: T[];
}

const anyPage: Paged = { page: 1, size: 20, items: [] };
const bookPage: Paged<{ title: string }> = {
  page: 1,
  size: 10,
  items: [{ title: "活着" }],
};
console.log(anyPage.size, bookPage.items[0].title);

泛型相互约束:key 必须是 obj 的键,返回值精确到属性类型:

function pick<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const book = { title: "活着", price: 39, stock: 8 };

const title: string = pick(book, "title");
const price: number = pick(book, "price");
// pick(book, "author"); // 若取消注释:"author" 不是 book 的键,编译报错
console.log(`${title} ¥${price}`);

常见坑

  • 把约束写成 T extends any:等于没约束,还掩盖了「其实不需要泛型」的代码味道。
  • 约束里写过于具体的类型:T extends string[] 太死,T extends readonly unknown[] 往往更通用;约束写「刚好够用」的最小接口。
  • 以为默认值参与推断:默认值只在「推断失败且未显式指定」时兜底;能从参数推出来就轮不到它。
  • keyof T 用于未约束的 T:keyof unknown、keyof any 的结果出乎意料(never 或 string | number | symbol),K extends keyof T 模式里 T 也要先约束成对象。

小结

extends 约束泛型的最低能力;= 提供兜底类型;T 与 K extends keyof T 的相互约束是类型安全取值器的标准写法。下一章把 keyof 和 typeof 这两个类型操作符讲透。