第69课:泛型(上)——泛型函数、泛型接口、泛型约束与默认类型
泛型(Generics)是 TypeScript 类型系统中最强大的特性之一。它允许你编写可复用、类型安全的组件,同时保留调用方传入的具体类型信息。简单来说,泛型就是“类型参数”——函数、接口或类可以接受一个或多个类型变量,在使用时由调用方指定(或由编译器推断)具体的类型,从而在保持灵活性的同时不丢失类型信息。本节课将聚焦于泛型的基础语法:如何在函数和接口中声明类型参数、如何对类型参数施加约束(限制其必须满足某些条件),以及如何提供默认类型以简化使用。
1. 泛型函数:让函数与类型共舞
1.1 从 any 到泛型:丢失类型信息的痛点
假设我们想编写一个函数 identity,它接收一个参数并原样返回。如果使用 any,调用方会丢失类型信息:
1 | function identity(value: any): any { |
identity 返回 any,导致后续使用 result 时没有任何类型检查——IDE 也不会提示字符串的方法。泛型解决了这一问题:
1 | function identity<T>(value: T): T { |
关键点:
<T>是类型参数,T是约定俗成的名字(也可用任何合法标识符,如TValue、TInput)。- 调用时可以在函数名后显式传入类型参数(
identity<string>),也可以交给编译器推断。 - 泛型函数保留了输入类型与输出类型的精确关系——输入
string,输出必然是string。
1.2 多类型参数
泛型可以同时接受多个类型参数,用逗号分隔:
1 | function pair<T, U>(first: T, second: U): [T, U] { |
1.3 泛型在箭头函数中的写法
在 .tsx 文件中,<T> 可能与 JSX 标签冲突。使用 extends 或添加尾随逗号可消除歧义:
1 | // 普通箭头函数泛型 |
2. 泛型接口与泛型类型别名
泛型不仅可以用于函数,还可以用于接口和类型别名,从而定义“可配置形状”的数据结构。
2.1 泛型接口
1 | interface Box<T> { |
常用场景:API 响应包装器。
1 | interface ApiResponse<T> { |
2.2 泛型类型别名
1 | type Predicate<T> = (value: T) => boolean; |
泛型类型别名非常擅长描述函数签名、联合类型和工具类型。
3. 泛型约束(Generic Constraints)
默认情况下,类型参数 T 可以是任何类型。但在某些场景下,你需要限制 T 必须满足某些条件——例如,T 必须拥有 .length 属性,或者 T 必须是一个对象。这时需要使用 extends 关键字施加约束。
3.1 基本约束
1 | interface HasLength { |
T extends HasLength 要求 T 必须是 HasLength 的子类型——即必须包含 length: number 属性。这样,在函数内部就可以安全地访问 item.length。
3.2 类型参数之间的约束
一个类型参数可以依赖另一个类型参数。
1 | function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { |
这里 K extends keyof T 约束了 K 必须是 T 的属性名之一,从而确保了 obj[key] 的类型安全,并返回精确的类型 T[K]。
3.3 约束与类型缩窄
泛型约束不仅限制了调用方可以传入的类型,还在函数内部缩窄了类型参数的可操作范围。没有约束时,你只能对 T 执行任何类型都支持的操作(如 typeof、===);有了约束,你就可以访问约束类型中的成员。
1 | function clone<T extends object>(obj: T): T { |
4. 泛型默认类型
你可以为类型参数提供默认类型。如果调用方没有显式指定类型参数,且 TypeScript 无法推断出具体类型时,将使用默认类型。
1 | interface ApiResponse<T = any> { |
默认类型简化了那些“常用默认类型”的泛型接口的使用。
默认类型与约束结合:
1 | interface Container<T extends object = { id: string }> { |
5. 实战:构建一个通用的缓存管理器
我们将综合运用泛型函数、泛型接口和泛型约束,构建一个类型安全的缓存管理器。
1 | interface CacheStore<T> { |
设计要点:
CacheStore<T>泛型接口定义了通用的缓存契约。MemoryCache<T>泛型类实现了该接口,T在实例化时确定。- 不同类型的数据拥有各自的缓存实例,并且完全类型安全。
课后练习
一、概念自测(选择题 / 填空题)
(单选) 以下哪个泛型约束表示
T必须拥有length: number属性?
A.T extends { length }
B.T extends { length: number }
C.T extends number
D.T extends Array<any>(单选) 关于泛型默认类型,以下描述正确的是?
A. 默认类型只能在函数中使用。
B. 如果调用方没有指定类型参数且无法推断,则使用默认类型。
C. 默认类型会覆盖类型推断。
D. 默认类型只能是any。(填空) 要约束类型参数
K必须是类型T的属性键之一,应写作K extends ______。(多选) 以下哪些是泛型的典型应用场景?
A. 通用的identity函数,保持输入输出类型一致
B. 包装 API 响应的通用接口ApiResponse<T>
C. 从对象中安全获取属性的函数getProperty(obj, key)
D. 直接替代any,无任何额外价值
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要编写一个 TypeScript 泛型工具函数 groupBy<T, K extends keyof T>(items: T[], key: K): Record<string, T[]>,它接收一个对象数组和一个属性键,按照该属性的值对数组进行分组,返回一个 Record 对象,其中键是分组的依据(转为字符串),值是对应的对象数组。要求:
- 使用泛型约束
K extends keyof T确保key是T的有效属性。 - 函数内部使用
reduce实现分组逻辑。 - 使用
const声明函数,添加 JSDoc 注释说明泛型参数和返回值。 - 提供至少两个测试示例(如按
status分组订单、按category分组商品)。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 TypeScript 代码。提示词中需明确指定泛型约束、Record 返回值类型和测试用例。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请创建 TypeScript 文件
src/groupBy.ts,实现泛型分组函数groupBy。要求:
- 函数签名:
function groupBy<T, K extends keyof T>(items: T[], key: K): Record<string, T[]>。- 使用
Array.prototype.reduce实现:遍历items,以String(item[key])作为分组的键,将当前项推入对应数组。- 为函数添加 JSDoc 注释,解释
T(数组元素类型)、K(分组的键)、返回值(Record)的含义。- 在文件末尾添加测试代码(使用
console.log验证):
- 订单数组
[{ id: 1, status: 'paid' }, { id: 2, status: 'pending' }, { id: 3, status: 'paid' }],按status分组。- 商品数组
[{ name: 'Apple', category: 'fruit' }, { name: 'Carrot', category: 'vegetable' }],按category分组。- 使用 ESM 导出函数,确保代码在严格模式下可编译。完成后列出完整文件内容。
四、面试真题与参考答案
题目(美团前端面试题):
请详细解释 TypeScript 中泛型的作用,以及泛型约束 (
extends) 的意义。给出一个示例,说明如何使用泛型约束来确保函数能够安全地访问参数的特定属性。
参考答案:
泛型允许你编写能够处理多种类型、同时保持类型安全的代码。它通过类型参数让类型在定义时保持抽象,在调用时变得具体,从而在复用与安全之间取得平衡。泛型约束 (T extends SomeType) 则限制了类型参数必须满足某个结构,从而在函数体内可以安全地访问该结构中的属性或方法。例如,<T extends { length: number }> 允许你安全地访问 item.length,否则 TypeScript 无法知道 T 是否拥有 length 属性。
典型示例:function logLength<T extends { length: number }>(item: T): T { console.log(item.length); return item; }。这里约束保证了 item.length 的合法访问,同时保留了 T 的精确类型(调用 logLength('hello') 后返回 'hello' 的类型为 string,而非 HasLength)。
课后练习答案
一、概念自测答案
B
- 解析:
T extends { length: number }约束了T必须拥有length: number属性。A 中{ length }是简写但需提供值,语法不正确;C、D 不匹配。
- 解析:
B
- 解析:泛型默认类型在调用方未指定且无法推断时生效。它不会覆盖推断结果。
keyof T- 解析:
K extends keyof T将K约束为T的键的联合类型。
- 解析:
A、B、C
- 解析:A、B、C 都是泛型的典型应用,D 误解了泛型的价值——泛型保留了类型信息,远胜于
any。
- 解析:A、B、C 都是泛型的典型应用,D 误解了泛型的价值——泛型保留了类型信息,远胜于
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请编写 TypeScript 函数groupBy(items, key),实现按指定键对对象数组分组。要求:
- 函数签名:
function groupBy<T, K extends keyof T>(items: T[], key: K): Record<string, T[]>。- 使用
reduce实现:初始值为{} as Record<string, T[]>,遍历items,以String(item[key])为键,将当前项追加到数组。- 添加 JSDoc 注释。
- 提供两个测试用例注释:订单按 status 分组、商品按 category 分组。
- 直接输出完整代码。”