Skip to content

泛型约束

概述

泛型约束(Generic Constraints)是 TypeScript 中用于限制泛型类型参数范围的重要特性。通过约束,我们可以要求泛型类型必须满足某些条件,比如必须具有某些属性、必须继承某个类型、或者必须是某个类型的子类型。泛型约束使用 extends 关键字来实现,它让我们在保持类型灵活性的同时,确保类型安全,并能够访问被约束类型的特定成员。

为什么需要泛型约束

在没有约束的情况下,泛型类型参数可以是任何类型,这导致我们无法访问类型的具体属性或方法:

typescript
// 没有约束:无法访问 length 属性
function getLength<T>(arg: T): number {
  // ❌ 错误:Property 'length' does not exist on type 'T'
  return arg.length;  // Error
}

// 使用 any:失去了类型安全
function getLengthAny(arg: any): number {
  return arg.length;  // 可以编译,但没有类型检查
}

使用泛型约束可以解决这个问题:

typescript
// 使用约束:T 必须具有 length 属性
function getLength<T extends { length: number }>(arg: T): number {
  return arg.length;  // ✅ 正确:现在可以访问 length 属性
}

// 可以用于字符串、数组等
const strLength = getLength("hello");     // 5
const arrLength = getLength([1, 2, 3]);   // 3

// ❌ 错误:数字没有 length 属性
// const numLength = getLength(42);  // Error

基本语法

使用 extends 约束

extends 关键字用于指定泛型类型参数必须满足的条件:

typescript
// 基本约束语法:T extends ConstraintType
function processValue<T extends string>(value: T): T {
  return value.toUpperCase() as T;
}

// 使用示例
const result = processValue("hello");  // 类型:string
// const num = processValue(42);  // ❌ 错误:number 不能赋值给 string

约束为接口或类型

最常见的约束方式是要求泛型类型必须实现某个接口或满足某个类型:

typescript
// 定义接口
interface HasLength {
  length: number;
}

// 约束泛型必须实现 HasLength 接口
function getLength<T extends HasLength>(arg: T): number {
  return arg.length;
}

// 使用示例
const strLength = getLength("hello");        // ✅ 字符串有 length
const arrLength = getLength([1, 2, 3]);      // ✅ 数组有 length
const objLength = getLength({ length: 10 }); // ✅ 对象有 length 属性

// ❌ 错误:数字没有 length 属性
// const numLength = getLength(42);  // Error

约束为对象类型

可以直接使用对象类型字面量作为约束:

typescript
// 约束为对象类型字面量
function getProperty<T extends { id: number; name: string }>(
  obj: T
): string {
  return `${obj.id}: ${obj.name}`;
}

// 使用示例
interface User {
  id: number;
  name: string;
  email: string;
}

const user: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com"
};

const result = getProperty(user);  // ✅ 正确:User 满足约束条件

使用 keyof 操作符

keyof 操作符用于获取类型的所有键的联合类型,常用于约束泛型类型参数必须是某个类型的键:

基本用法

typescript
// 使用 keyof 约束键的类型
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// 使用示例
interface User {
  id: number;
  name: string;
  email: string;
}

const user: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com"
};

// ✅ 正确:key 必须是 User 的键之一
const id = getProperty(user, "id");      // 类型:number
const name = getProperty(user, "name");  // 类型:string
const email = getProperty(user, "email"); // 类型:string

// ❌ 错误:'age' 不是 User 的键
// const age = getProperty(user, "age");  // Error

keyof 与索引访问结合

keyof 经常与索引访问类型 T[K] 结合使用,实现类型安全的属性访问:

typescript
// 获取对象属性的值,类型安全
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// 设置对象属性的值,类型安全
function setValue<T, K extends keyof T>(
  obj: T,
  key: K,
  value: T[K]
): void {
  obj[key] = value;
}

// 使用示例
interface Config {
  host: string;
  port: number;
  ssl: boolean;
}

const config: Config = {
  host: "localhost",
  port: 3000,
  ssl: true
};

// ✅ 类型安全:TypeScript 知道返回值的类型
const host = getValue(config, "host");  // 类型:string
const port = getValue(config, "port");  // 类型:number

// ✅ 类型安全:TypeScript 检查值的类型
setValue(config, "port", 8080);  // ✅ 正确
// setValue(config, "port", "8080");  // ❌ 错误:类型不匹配

多重约束

TypeScript 支持对泛型类型参数应用多个约束,使用 &(交叉类型)连接:

typescript
// 多重约束:T 必须同时满足多个条件
interface HasId {
  id: number;
}

interface HasName {
  name: string;
}

// T 必须同时实现 HasId 和 HasName
function processEntity<T extends HasId & HasName>(entity: T): string {
  return `${entity.id}: ${entity.name}`;
}

// 使用示例
interface User extends HasId, HasName {
  email: string;
}

const user: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com"
};

const result = processEntity(user);  // ✅ 正确:User 满足所有约束

约束链

可以创建约束链,一个约束依赖于另一个约束:

typescript
// 约束链:U 必须继承 T,T 必须具有 id 属性
function findById<T extends { id: number }, U extends T>(
  items: U[],
  id: number
): U | undefined {
  return items.find(item => item.id === id);
}

// 使用示例
interface BaseEntity {
  id: number;
}

interface User extends BaseEntity {
  name: string;
}

const users: User[] = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" }
];

const user = findById(users, 1);  // 类型:User | undefined

约束泛型类

泛型类也可以使用约束:

typescript
// 约束泛型类
interface Identifiable {
  id: number;
}

class Repository<T extends Identifiable> {
  private items: T[] = [];
  
  add(item: T): void {
    this.items.push(item);
  }
  
  findById(id: number): T | undefined {
    return this.items.find(item => item.id === id);
  }
  
  getAll(): T[] {
    return [...this.items];
  }
}

// 使用示例
interface User extends Identifiable {
  name: string;
  email: string;
}

const userRepo = new Repository<User>();
userRepo.add({ id: 1, name: "Alice", email: "alice@example.com" });
userRepo.add({ id: 2, name: "Bob", email: "bob@example.com" });

const user = userRepo.findById(1);  // 类型:User | undefined

使用示例

示例 1:数组工具函数

typescript
// 约束数组元素必须具有 id 属性
interface Identifiable {
  id: number;
}

// 根据 id 查找元素
function findById<T extends Identifiable>(
  items: T[],
  id: number
): T | undefined {
  return items.find(item => item.id === id);
}

// 根据 id 删除元素
function removeById<T extends Identifiable>(
  items: T[],
  id: number
): T[] {
  return items.filter(item => item.id !== id);
}

// 使用示例
interface Product extends Identifiable {
  name: string;
  price: number;
}

const products: Product[] = [
  { id: 1, name: "Laptop", price: 999 },
  { id: 2, name: "Mouse", price: 29 }
];

const product = findById(products, 1);  // 类型:Product | undefined
const filtered = removeById(products, 2);  // 类型:Product[]

示例 2:对象属性操作

typescript
// 安全地获取对象属性
function safeGet<T, K extends keyof T>(
  obj: T,
  key: K,
  defaultValue: T[K]
): T[K] {
  return obj[key] ?? defaultValue;
}

// 安全地设置对象属性
function safeSet<T, K extends keyof T>(
  obj: T,
  key: K,
  value: T[K]
): void {
  obj[key] = value;
}

// 使用示例
interface Config {
  host: string;
  port: number;
  timeout: number;
}

const config: Config = {
  host: "localhost",
  port: 3000,
  timeout: 5000
};

// ✅ 类型安全:TypeScript 知道返回值的类型
const host = safeGet(config, "host", "default");  // 类型:string
const port = safeGet(config, "port", 8080);       // 类型:number

// ✅ 类型安全:TypeScript 检查值的类型
safeSet(config, "port", 8080);  // ✅ 正确
// safeSet(config, "port", "8080");  // ❌ 错误:类型不匹配

示例 3:API 响应处理

typescript
// 约束 API 响应必须具有 status 属性
interface ApiResponse {
  status: "success" | "error";
}

// 处理 API 响应
function handleResponse<T extends ApiResponse>(
  response: T
): T["status"] extends "success" ? T : null {
  if (response.status === "success") {
    return response as any;
  }
  return null as any;
}

// 使用示例
interface UserResponse extends ApiResponse {
  status: "success";
  data: {
    id: number;
    name: string;
  };
}

interface ErrorResponse extends ApiResponse {
  status: "error";
  message: string;
}

const successResponse: UserResponse = {
  status: "success",
  data: { id: 1, name: "Alice" }
};

const result = handleResponse(successResponse);

示例 4:类型安全的对象合并

typescript
// 合并两个对象,保持类型安全
function merge<T extends object, U extends object>(
  target: T,
  source: U
): T & U {
  return { ...target, ...source };
}

// 使用示例
interface User {
  id: number;
  name: string;
}

interface UserDetails {
  email: string;
  age: number;
}

const user: User = {
  id: 1,
  name: "Alice"
};

const details: UserDetails = {
  email: "alice@example.com",
  age: 25
};

// ✅ 类型安全:返回类型是 T & U
const merged = merge(user, details);
// 类型:User & UserDetails
// 可以访问所有属性
console.log(merged.id);    // 1
console.log(merged.name);   // "Alice"
console.log(merged.email); // "alice@example.com"
console.log(merged.age);   // 25

示例 5:提取对象属性

typescript
// 提取对象的指定属性
function pick<T, K extends keyof T>(
  obj: T,
  keys: K[]
): Pick<T, K> {
  const result = {} as Pick<T, K>;
  keys.forEach(key => {
    result[key] = obj[key];
  });
  return result;
}

// 使用示例
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

const user: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com",
  age: 25
};

// ✅ 类型安全:只提取指定的属性
const publicInfo = pick(user, ["name", "email"]);
// 类型:Pick<User, "name" | "email">
// 只包含 name 和 email 属性

示例 6:条件约束

typescript
// 根据条件应用不同的约束
type Constraint<T> = T extends string
  ? { length: number }
  : T extends number
  ? { valueOf(): number }
  : {};

function processValue<T>(
  value: T & Constraint<T>
): void {
  // 根据类型处理
}

// 更实用的例子:处理具有特定属性的对象
interface HasId {
  id: number;
}

interface HasName {
  name: string;
}

// 处理具有 id 的对象
function processById<T extends HasId>(item: T): void {
  console.log(`Processing item with id: ${item.id}`);
}

// 处理具有 name 的对象
function processByName<T extends HasName>(item: T): void {
  console.log(`Processing item with name: ${item.name}`);
}

// 处理同时具有 id 和 name 的对象
function processByIdAndName<T extends HasId & HasName>(item: T): void {
  console.log(`Processing: ${item.id} - ${item.name}`);
}

// 使用示例
interface User extends HasId, HasName {
  email: string;
}

const user: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com"
};

processById(user);           // ✅ 正确
processByName(user);          // ✅ 正确
processByIdAndName(user);     // ✅ 正确

约束与类型推断

泛型约束会影响类型推断的行为:

typescript
// 约束影响类型推断
function createArray<T extends string | number>(
  length: number,
  value: T
): T[] {
  return Array(length).fill(value);
}

// 类型推断
const strings = createArray(5, "hello");  // 类型:string[]
const numbers = createArray(5, 42);      // 类型:number[]

// ❌ 错误:boolean 不满足约束
// const booleans = createArray(5, true);  // Error

约束与默认类型参数

约束可以与默认类型参数结合使用:

typescript
// 约束与默认类型参数
interface DefaultConfig {
  timeout: number;
  retries: number;
}

function createConfig<T extends DefaultConfig = DefaultConfig>(
  config: Partial<T> = {}
): T {
  return {
    timeout: 5000,
    retries: 3,
    ...config
  } as T;
}

// 使用示例
const defaultConfig = createConfig();  // 类型:DefaultConfig

interface CustomConfig extends DefaultConfig {
  apiKey: string;
}

const customConfig = createConfig<CustomConfig>({
  apiKey: "abc123"
});  // 类型:CustomConfig

类型检查示例

常见错误

typescript
// ❌ 错误:不满足约束条件
interface HasLength {
  length: number;
}

function getLength<T extends HasLength>(arg: T): number {
  return arg.length;
}

// const numLength = getLength(42);  // Error: number 不满足 HasLength 约束

// ❌ 错误:keyof 约束的键不存在
interface User {
  id: number;
  name: string;
}

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

const user: User = { id: 1, name: "Alice" };
// const email = getProperty(user, "email");  // Error: 'email' 不是 User 的键

// ❌ 错误:多重约束不满足
interface HasId {
  id: number;
}

interface HasName {
  name: string;
}

function process<T extends HasId & HasName>(item: T): void {
  console.log(item.id, item.name);
}

interface User {
  id: number;
  // 缺少 name 属性
}

const user: User = { id: 1 };
// process(user);  // Error: User 不满足 HasId & HasName 约束

正确写法

typescript
// ✅ 正确:满足约束条件
interface HasLength {
  length: number;
}

function getLength<T extends HasLength>(arg: T): number {
  return arg.length;
}

const strLength = getLength("hello");        // ✅ 正确
const arrLength = getLength([1, 2, 3]);      // ✅ 正确
const objLength = getLength({ length: 10 }); // ✅ 正确

// ✅ 正确:使用 keyof 约束
interface User {
  id: number;
  name: string;
  email: string;
}

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

const user: User = { id: 1, name: "Alice", email: "alice@example.com" };
const id = getProperty(user, "id");      // ✅ 正确:类型 number
const name = getProperty(user, "name");  // ✅ 正确:类型 string

// ✅ 正确:满足多重约束
interface HasId {
  id: number;
}

interface HasName {
  name: string;
}

function process<T extends HasId & HasName>(item: T): void {
  console.log(item.id, item.name);
}

interface User extends HasId, HasName {
  email: string;
}

const user: User = { id: 1, name: "Alice", email: "alice@example.com" };
process(user);  // ✅ 正确:User 满足所有约束

注意事项

提示

  • 泛型约束使用 extends 关键字,表示"必须满足"或"必须继承"的关系
  • keyof 操作符用于获取类型的所有键,常用于约束键的类型
  • 约束可以链式使用,一个约束可以依赖于另一个约束
  • 多重约束使用 &(交叉类型)连接多个约束条件
  • 约束不会影响运行时的行为,只在编译时进行类型检查

注意

  • 约束过于严格可能会限制泛型的灵活性,需要权衡类型安全和灵活性
  • 使用 keyof 时,确保传入的键确实存在于对象类型中
  • 多重约束要求类型必须同时满足所有约束条件,否则会出现类型错误
  • 约束的类型参数在函数内部可以访问被约束类型的成员,但需要确保约束条件正确

重要

  • 泛型约束是 TypeScript 类型系统的核心特性,理解约束对于掌握高级类型编程非常重要
  • keyof 操作符与索引访问类型 T[K] 结合使用,可以实现类型安全的属性访问
  • 约束与条件类型、映射类型等高级特性结合使用,可以构建强大的类型系统
  • 合理使用约束可以在保持类型安全的同时,提高代码的复用性和灵活性

信息

泛型约束的优势

  • 类型安全:确保泛型类型满足特定条件,避免运行时错误
  • 更好的 IDE 支持:约束让 TypeScript 能够提供更准确的类型提示
  • 代码复用:通过约束,一个函数可以处理多种满足条件的类型
  • 自文档化:约束条件本身就是文档,说明了函数对类型的要求

泛型约束的应用场景

  • 对象属性操作(使用 keyof
  • 数据结构操作(数组、集合等)
  • API 响应处理
  • 工具函数库开发
  • 框架和库的类型定义

相关链接

基于 VitePress 构建