首页 / TypeScript 入门教程 / 泛型约束

TypeScript 入门教程

泛型约束

本教程共 80 篇 · 第 39 篇 · 更新于 2026-08-10 · 约 10 分钟阅读

TypeScriptTypeScript 入门教程泛型泛型约束

本节目标:学会用 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 三个场景覆盖了大部分实际需求:

  1. 限制类型参数必须有某个属性或形状——T extends { length: number }
  2. 限制类型参数必须是另一个类型的键——K extends keyof T
  3. 类型参数之间相互约束——U extends keyof T

再加一个默认类型 T = SomeType,泛型就同时具备了灵活性和安全边界。下一章我们看看类级别和接口级别的泛型约束能做什么。