WinddSnow

TypeScript-Generics-Part1-Functions-Interfaces-Constraints

字数统计: 3.1k阅读时长: 13 min
2026/07/30

第69课:泛型(上)——泛型函数、泛型接口、泛型约束与默认类型

泛型(Generics)是 TypeScript 类型系统中最强大的特性之一。它允许你编写可复用类型安全的组件,同时保留调用方传入的具体类型信息。简单来说,泛型就是“类型参数”——函数、接口或类可以接受一个或多个类型变量,在使用时由调用方指定(或由编译器推断)具体的类型,从而在保持灵活性的同时不丢失类型信息。本节课将聚焦于泛型的基础语法:如何在函数和接口中声明类型参数、如何对类型参数施加约束(限制其必须满足某些条件),以及如何提供默认类型以简化使用。


1. 泛型函数:让函数与类型共舞

1.1 从 any 到泛型:丢失类型信息的痛点

假设我们想编写一个函数 identity,它接收一个参数并原样返回。如果使用 any,调用方会丢失类型信息:

1
2
3
4
5
6
function identity(value: any): any {
return value;
}

const result = identity('hello');
// result 的类型为 any,而不是 string

identity 返回 any,导致后续使用 result 时没有任何类型检查——IDE 也不会提示字符串的方法。泛型解决了这一问题:

1
2
3
4
5
6
7
8
9
function identity<T>(value: T): T {
return value;
}

const str = identity<string>('hello'); // str: string
const num = identity<number>(42); // num: number

// 类型推断:如果不显式指定类型参数,TypeScript 会自动推断
const inferred = identity('world'); // inferred: string

关键点

  • <T>类型参数T 是约定俗成的名字(也可用任何合法标识符,如 TValueTInput)。
  • 调用时可以在函数名后显式传入类型参数(identity<string>),也可以交给编译器推断。
  • 泛型函数保留了输入类型与输出类型的精确关系——输入 string,输出必然是 string

1.2 多类型参数

泛型可以同时接受多个类型参数,用逗号分隔:

1
2
3
4
5
6
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}

const p = pair('age', 25); // p: [string, number]
const q = pair(true, { name: 'Alice' }); // q: [boolean, { name: string }]

1.3 泛型在箭头函数中的写法

.tsx 文件中,<T> 可能与 JSX 标签冲突。使用 extends 或添加尾随逗号可消除歧义:

1
2
3
4
5
6
7
// 普通箭头函数泛型
const identity = <T>(value: T): T => value;

// 在 .tsx 文件中,推荐添加尾随逗号或使用 extends
const identityTsx = <T,>(value: T): T => value;
// 或
const identityTsx2 = <T extends unknown>(value: T): T => value;

2. 泛型接口与泛型类型别名

泛型不仅可以用于函数,还可以用于接口类型别名,从而定义“可配置形状”的数据结构。

2.1 泛型接口

1
2
3
4
5
6
interface Box<T> {
value: T;
}

const stringBox: Box<string> = { value: 'hello' };
const numberBox: Box<number> = { value: 42 };

常用场景:API 响应包装器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}

type User = { id: number; name: string };
type UserListResponse = ApiResponse<User[]>;

const response: UserListResponse = {
code: 200,
message: 'success',
data: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
};

2.2 泛型类型别名

1
2
3
4
type Predicate<T> = (value: T) => boolean;

const isLongString: Predicate<string> = (s) => s.length > 10;
const isPositive: Predicate<number> = (n) => n > 0;

泛型类型别名非常擅长描述函数签名、联合类型和工具类型。


3. 泛型约束(Generic Constraints)

默认情况下,类型参数 T 可以是任何类型。但在某些场景下,你需要限制 T 必须满足某些条件——例如,T 必须拥有 .length 属性,或者 T 必须是一个对象。这时需要使用 extends 关键字施加约束。

3.1 基本约束

1
2
3
4
5
6
7
8
9
10
11
12
13
interface HasLength {
length: number;
}

function logLength<T extends HasLength>(item: T): T {
console.log(`Length: ${item.length}`);
return item;
}

logLength('hello'); // Length: 5
logLength([1, 2, 3]); // Length: 3
logLength({ length: 10 }); // Length: 10
// logLength(123); // ❌ 类型“number”不满足约束“HasLength”

T extends HasLength 要求 T 必须是 HasLength 的子类型——即必须包含 length: number 属性。这样,在函数内部就可以安全地访问 item.length

3.2 类型参数之间的约束

一个类型参数可以依赖另一个类型参数。

1
2
3
4
5
6
7
8
9
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

const user = { name: 'Alice', age: 25, email: 'alice@example.com' };

const name: string = getProperty(user, 'name'); // ✅ 返回 string
const age: number = getProperty(user, 'age'); // ✅ 返回 number
// const invalid = getProperty(user, 'address'); // ❌ 错误:'address' 不是 user 的键

这里 K extends keyof T 约束了 K 必须是 T 的属性名之一,从而确保了 obj[key] 的类型安全,并返回精确的类型 T[K]

3.3 约束与类型缩窄

泛型约束不仅限制了调用方可以传入的类型,还在函数内部缩窄了类型参数的可操作范围。没有约束时,你只能对 T 执行任何类型都支持的操作(如 typeof===);有了约束,你就可以访问约束类型中的成员。

1
2
3
4
5
6
function clone<T extends object>(obj: T): T {
return { ...obj };
}

clone({ a: 1, b: 2 }); // ✅
// clone(42); // ❌ 类型“number”不满足约束“object”

4. 泛型默认类型

你可以为类型参数提供默认类型。如果调用方没有显式指定类型参数,且 TypeScript 无法推断出具体类型时,将使用默认类型。

1
2
3
4
5
6
7
8
9
10
interface ApiResponse<T = any> {
code: number;
data: T;
}

// 不指定 T,默认为 any
const defaultResp: ApiResponse = { code: 200, data: { foo: 'bar' } };

// 显式指定 T
const userResp: ApiResponse<User> = { code: 200, data: { id: 1, name: 'Alice' } };

默认类型简化了那些“常用默认类型”的泛型接口的使用。

默认类型与约束结合

1
2
3
4
5
6
7
8
9
10
11
interface Container<T extends object = { id: string }> {
value: T;
}

// 不指定 T,默认为 { id: string }
const defaultContainer: Container = { value: { id: 'abc' } };

// 指定更具体的类型
const userContainer: Container<{ id: string; name: string }> = {
value: { id: '1', name: 'Alice' }
};

5. 实战:构建一个通用的缓存管理器

我们将综合运用泛型函数、泛型接口和泛型约束,构建一个类型安全的缓存管理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
interface CacheStore<T> {
get(key: string): T | undefined;
set(key: string, value: T): void;
remove(key: string): void;
}

class MemoryCache<T> implements CacheStore<T> {
private store = new Map<string, T>();

get(key: string): T | undefined {
return this.store.get(key);
}

set(key: string, value: T): void {
this.store.set(key, value);
}

remove(key: string): void {
this.store.delete(key);
}
}

// 使用字符串缓存
const stringCache = new MemoryCache<string>();
stringCache.set('token', 'abc123');
const token = stringCache.get('token'); // token: string | undefined

// 使用用户对象缓存
interface User {
id: number;
name: string;
}
const userCache = new MemoryCache<User>();
userCache.set('user:1', { id: 1, name: 'Alice' });
const user = userCache.get('user:1'); // user: User | undefined

设计要点

  • CacheStore<T> 泛型接口定义了通用的缓存契约。
  • MemoryCache<T> 泛型类实现了该接口,T 在实例化时确定。
  • 不同类型的数据拥有各自的缓存实例,并且完全类型安全。

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) 以下哪个泛型约束表示 T 必须拥有 length: number 属性?
    A. T extends { length }
    B. T extends { length: number }
    C. T extends number
    D. T extends Array<any>

  2. (单选) 关于泛型默认类型,以下描述正确的是?
    A. 默认类型只能在函数中使用。
    B. 如果调用方没有指定类型参数且无法推断,则使用默认类型。
    C. 默认类型会覆盖类型推断。
    D. 默认类型只能是 any

  3. (填空) 要约束类型参数 K 必须是类型 T 的属性键之一,应写作 K extends ______

  4. (多选) 以下哪些是泛型的典型应用场景?
    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 确保 keyT 的有效属性。
  • 函数内部使用 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)。


课后练习答案

一、概念自测答案

  1. B

    • 解析:T extends { length: number } 约束了 T 必须拥有 length: number 属性。A 中 { length } 是简写但需提供值,语法不正确;C、D 不匹配。
  2. B

    • 解析:泛型默认类型在调用方未指定且无法推断时生效。它不会覆盖推断结果。
  3. keyof T

    • 解析:K extends keyof TK 约束为 T 的键的联合类型。
  4. A、B、C

    • 解析:A、B、C 都是泛型的典型应用,D 误解了泛型的价值——泛型保留了类型信息,远胜于 any

二、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 分组。
  • 直接输出完整代码。”
CATALOG
  1. 1. 第69课:泛型(上)——泛型函数、泛型接口、泛型约束与默认类型
    1. 1.1. 1. 泛型函数:让函数与类型共舞
      1. 1.1.1. 1.1 从 any 到泛型:丢失类型信息的痛点
      2. 1.1.2. 1.2 多类型参数
      3. 1.1.3. 1.3 泛型在箭头函数中的写法
    2. 1.2. 2. 泛型接口与泛型类型别名
      1. 1.2.1. 2.1 泛型接口
      2. 1.2.2. 2.2 泛型类型别名
    3. 1.3. 3. 泛型约束(Generic Constraints)
      1. 1.3.1. 3.1 基本约束
      2. 1.3.2. 3.2 类型参数之间的约束
      3. 1.3.3. 3.3 约束与类型缩窄
    4. 1.4. 4. 泛型默认类型
    5. 1.5. 5. 实战:构建一个通用的缓存管理器
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.6.3. 三、Agent 模式下的提示词示例
      4. 1.6.4. 四、面试真题与参考答案
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)