Menu

TypeScript 泛型约束:extends 与 keyof T

泛型约束 T extends Something 限制类型参数能接受哪些类型,并让函数可以使用约束所保证的内容。内容包括配合对象结构和接口使用 extends、用 K extends keyof T 安全访问属性、对基本类型的约束,以及你会遇到的报错。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

泛型约束限制了类型参数可以是什么。<T extends { length: number }> 只接受带有数值 length 的类型(字符串、数组、你自己的对象),正因为有这个保证,函数才可以读取 .length。

返回类型仍然是 T,而不是 { length: number }:传入两个数字数组,得到的是 number[],对象也保留了它们的 tag。约束描述的是最低要求;调用方的类型会被保留下来。

为什么需要约束

在泛型函数内部,没有约束的 T 可以是任何东西:字符串、数字、null、函数。所以 TypeScript 只允许对所有类型都有效的操作,而这几乎什么都没有。

编译器输出:

index.ts(3,12): error TS2339: Property 'length' does not exist on type 'T'.
index.ts(3,24): error TS2339: Property 'length' does not exist on type 'T'.

解决办法是说明你需要什么:<T extends { length: number }>。约束把“任何类型”变成了“任何具有这些内容的类型”,而这通常正是泛型函数想表达的意思。

约束为接口

约束可以是任何类型,包括你已有的接口或类型别名。这样函数就能处理至少拥有这些成员的任何东西,同时仍然返回调用方的完整类型。

和普通的参数类型对比一下:function byId(items: HasId[], id: number): HasId | undefined 接受同样的数组,但结果只是 HasId,所以 found.name 会报错。泛型版本则把完整的类型传递了出来。

K extends keyof T:安全的属性访问

最常见的约束是把两个类型参数关联起来。K extends keyof T 表示 K 必须是 T 的某个属性名,而 T[K] 就是该属性的类型。

拼错或不存在的键会产生编译错误,返回类型也跟着键变化:"year" 得到 number,"title" 得到 string。keyof 本身的内容,包括它和索引签名一起使用时的表现,见 keyof 页面。

约束为基本类型

约束可以是一个基本类型,也可以是若干基本类型的联合。一个有用的副作用是:当 T extends string(或 number)时,TypeScript 会推断出实参的字面量类型,而不是把它拓宽。

没有约束时,createEvent("user.saved") 返回 { type: string }:这里 TypeScript 把它为 T 推断出的字面量拓宽成了 string。有了 T extends string,字面量就被保留下来,带类型的事件和路由辅助函数正是这样从普通字符串参数中得到精确名称的。

常见错误

返回只符合约束的值。 返回 T 的函数必须返回一个 T,而不是随便一个符合约束的值:

interface HasId {
  id: number;
}

function reset<T extends HasId>(item: T): T {
  return { id: item.id };
}
// error TS2322: Type '{ id: number; }' is not assignable to type 'T'.
//   '{ id: number; }' is assignable to the constraint of type 'T', but 'T' could be
//   instantiated with a different subtype of constraint 'HasId'.

如果 T 是 { id: number; name: string },新对象没有 name,所以它不是 T。返回输入的展开 return { ...item, id: 0 };,这样能保留所有属性;或者如果你产生的就只有这些,就把返回类型声明为 HasId。

传入违反约束的类型实参。 显式的类型实参同样会被检查:对于 function scale<T extends number>(x: T),调用 scale<string>("2") 会失败,报错 Type 'string' does not satisfy the constraint 'number'.(TS2344)。

不需要泛型时也加约束。 如果 T 只出现在参数中,那么 function print<T extends HasId>(item: T): void 就只是多了些语法的 function print(item: HasId): void。只有当 T 还出现在返回类型或另一个参数中时,约束才有意义。

带默认值的约束

类型参数可以同时拥有约束和默认值。默认值必须满足约束。

interface Store<TState extends object = Record<string, unknown>> {
  get(): TState;
  set(next: Partial<TState>): void;
}

type AnyStore = Store;                              // Store<Record<string, unknown>>
type CounterStore = Store<{ count: number }>;       // fine
type BadStore = Store<number>;
// error TS2344: Type 'number' does not satisfy the constraint 'object'.

同样的 extends 关键字也出现在条件类型 T extends string ? A : B 中,在那里它是一个判断而不是限制;这种形式见条件类型页面。

常见问题

TypeScript 泛型中的 T extends 是什么意思?

<T extends Constraint> 表示 T 可以是任何能赋值给 Constraint 的类型。调用方只能传入这样的类型,而在函数内部,你可以使用约束所保证的一切。<T extends { length: number }> 接受字符串、数组以及任何带有数值 length 的对象,并允许你读取 value.length。

K extends keyof T 是什么意思?

K 必须是 T 的某个属性名。配合索引访问类型 T[K],它可以精确地给属性访问标注类型:function get<T, K extends keyof T>(obj: T, key: K): T[K] 只接受 obj 真实存在的键,返回类型就是该属性的类型。

为什么会出现 “Property does not exist on type T”?

没有约束的 T 可以是任何类型,包括 number 或 null,所以 TypeScript 不允许在它上面做任何特定于某种类型的操作(TS2339)。添加一个保证该属性存在的约束:<T extends { name: string }>,或者约束为你已有的接口:<T extends User>。

“T could be instantiated with a different subtype of constraint” 是什么意思?

这是 TS2322 错误:函数承诺返回 T,返回的却是一个只符合约束的值。如果 T 是 { id: number; name: string },那么新建的 { id: 1 } 就不是 T。返回你收到的那个值(或者它的展开,并相应地标注类型),或者把返回类型改成约束类型。

泛型中的 extends 和类或接口中的 extends 有什么区别?

关键字相同,思想相关。在 class Dog extends Animal 和 interface B extends A 中,它声明的是继承。在 <T extends A> 中,它声明的是约束:T 必须能赋值给 A。在条件类型 T extends A ? X : Y 中,它是一个判断,从两个类型中选出一个。

Coddy 编程语言插图

和 Coddy 一起学编程

开始