← 返回导航

泛型约束详解

工具类型 · 中级

场景说明

extends 在 TypeScript 中有三种不同的用法:接口继承、条件类型、泛型约束。泛型约束是最常用的模式,通过 T extends SomeType 限制泛型参数的类型范围,让函数体内可以安全地访问泛型参数的属性。

完整代码


      
// extends 的三种用法与泛型约束实战

// 用法一:接口继承(interface extends)
interface Animal { name: string; }
interface Dog extends Animal { breed: string; }
// Dog 继承 Animal 的所有属性

// 用法二:条件类型(T extends U ? X : Y)
type IsString<T> = T extends string ? true : false;
type A = IsString<string>;   // true
type B = IsString<number>;   // false

// 用法三:泛型约束(T extends Constraint)
// 约束 T 必须包含特定属性
function getLength<T extends { length: number }>(arg: T): number {
  return arg.length;  // 安全,因为约束保证了 length 存在
}
getLength('hello');       // 5
getLength([1, 2, 3]);    // 3
// getLength(123);  // 错误!number 没有 length

// extends keyof:约束泛型为对象的键
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const user = { id: 1, name: 'Alice', email: 'alice@test.com' };
const id = getProperty(user, 'id');      // number
const name = getProperty(user, 'name');  // string
// getProperty(user, 'age');  // 错误!"age" 不是 "id" | "name" | "email"

// extends object vs extends Record<string, any>
// extends object:排除原始类型(string/number/boolean 等)
function fn1<T extends object>(arg: T) {}
fn1({});  // OK
fn1([]);  // OK
// fn1('hello');  // 错误!string 不是 object

// extends Record<string, any>:只接受有字符串索引签名的对象
function fn2<T extends Record<string, any>>(arg: T) {}
fn2({ a: 1 });  // OK
// fn2([]);  // 错误!数组没有 string 索引签名

// 多重约束:T 必须同时满足多个条件
interface HasId { id: number; }
interface HasName { name: string; }
function process<T extends HasId & HasName>(item: T) {
  console.log(item.id, item.name);  // 两个属性都安全可用
}

关键点解析

使用示例与类型推导


      
// 实战:封装一个通用的排序函数

interface Sortable {
  id: number;
  createdAt: string;
  [key: string]: unknown;
}

// T 必须是 Sortable 的子类型,K 必须是 T 的键
function sortBy<T extends Sortable, K extends keyof T>(
  list: T[],
  key: K,
  order: 'asc' | 'desc' = 'asc'
): T[] {
  return [...list].sort((a, b) => {
    const va = a[key];
    const vb = b[key];
    if (typeof va === 'string' && typeof vb === 'string') {
      return order === 'asc' ? va.localeCompare(vb) : vb.localeCompare(va);
    }
    return order === 'asc' ? Number(va) - Number(vb) : Number(vb) - Number(va);
  });
}

interface Product extends Sortable {
  name: string;
  price: number;
  stock: number;
}

const products: Product[] = [
  { id: 1, name: 'A', price: 100, stock: 10, createdAt: '2024-01-01' },
  { id: 2, name: 'B', price: 50, stock: 20, createdAt: '2024-01-02' },
];

const sorted = sortBy(products, 'price', 'desc');
// sortBy(products, 'unknown');  // 编译错误!
在 Playground 中尝试