第 101 章:泛型(Generics)
学习目标
- 理解泛型的核心思想和应用场景
- 掌握泛型函数、泛型接口、泛型类的写法
- 学会泛型约束与默认值
- 区分泛型与
any/unknown的边界
一、为什么需要泛型
泛型解决的是「类型的复用性」问题。
1.1 没有泛型时的痛苦
typescript
// ❌ 每个类型写一遍函数
function identityString(value: string): string {
return value;
}
function identityNumber(value: number): number {
return value;
}
// 想支持所有类型?用 any
function identityAny(value: any): any {
return value; // ❌ 返回值丢失类型
}1.2 用泛型解决
typescript
// ✅ 一个函数支持所有类型,且保留类型信息
function identity<T>(value: T): T {
return value;
}
const a = identity('hello'); // 类型: string
const b = identity(123); // 类型: number
const c = identity({ x: 1 }); // 类型: { x: number }二、泛型基础
2.1 泛型函数
typescript
// 语法: 在函数名后加 <T>
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
first([1, 2, 3]); // 类型: number | undefined
first(['a', 'b']); // 类型: string | undefined
first([]); // 类型: undefined
// 箭头函数写法
const firstArrow = <T,>(arr: T[]): T | undefined => arr[0];
// ^ 加逗号避免与 JSX 冲突2.2 显式指定类型参数
typescript
function identity<T>(value: T): T {
return value;
}
// TS 自动推断
const a = identity(123); // T = number
// 显式指定
const b = identity<string>('hi'); // T = string
// 多参数
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
pair('age', 18); // [string, number]
pair<number, string>(1, 'Tom'); // [number, string]2.3 多个类型参数
typescript
function swap<A, B>(tuple: [A, B]): [B, A] {
return [tuple[1], tuple[0]];
}
const result = swap(['hello', 123]); // [number, string]三、泛型接口
3.1 函数类型接口
typescript
interface Mapper<T, U> {
(item: T): U;
}
const numberToString: Mapper<number, string> = (n) => String(n);
const stringLength: Mapper<string, number> = (s) => s.length;3.2 数据结构接口
typescript
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
interface User {
id: number;
name: string;
}
const res1: ApiResponse<User> = {
code: 0,
message: 'ok',
data: { id: 1, name: 'Tom' }
};
const res2: ApiResponse<User[]> = {
code: 0,
message: 'ok',
data: [{ id: 1, name: 'Tom' }]
};3.3 内置泛型接口
typescript
// Array<T> 本身就是泛型
const nums: Array<number> = [1, 2, 3];
// Promise<T>
const p: Promise<string> = new Promise((resolve) => resolve('hi'));
// Record<K, V>
type PageInfo = Record<string, number>;四、泛型类
typescript
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
setValue(value: T): void {
this.value = value;
}
}
const numberBox = new Container(123); // Container<number>
numberBox.getValue(); // 123
const stringBox = new Container<string>('hello'); // 显式指定4.1 实战:通用栈
typescript
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
const stack = new Stack<number>();
stack.push(1);
stack.push(2);
stack.pop(); // 2五、泛型约束(Constraints)
5.1 基础约束
默认 T 可以是任何类型,但有时我们需要限制。
typescript
// ❌ T 没有 length 属性,会报错
function logLength<T>(arg: T): void {
console.log(arg.length); // ❌ Property 'length' does not exist on type 'T'
}
// ✅ 用 extends 约束
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): void {
console.log(arg.length); // ✅ T 一定有 length
}
logLength('hello'); // ✅ string 有 length
logLength([1, 2, 3]); // ✅ array 有 length
logLength({ length: 10 }); // ✅ 对象有 length
logLength(123); // ❌ number 没有 length5.2 keyof 约束
typescript
// 约束 key 必须是对象 T 的键
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Tom', age: 18 };
getProp(user, 'name'); // ✅ string
getProp(user, 'age'); // ✅ number
getProp(user, 'hobby'); // ❌ 'hobby' 不在 user 的键中5.3 多重约束
typescript
interface A { a: () => void }
interface B { b: () => void }
function process<T extends A & B>(obj: T): void {
obj.a();
obj.b();
}
process({ a() {}, b() {} }); // ✅六、泛型默认值
typescript
// T 默认是 string
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const arr1 = createArray(3, 'x'); // string[]
const arr2 = createArray<number>(3, 0); // number[]七、泛型与条件类型
typescript
// 提取函数返回值类型
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function foo(): number { return 1; }
type FooReturn = MyReturnType<typeof foo>; // number
// 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type NumElement = ElementOf<number[]>; // number
type StrElement = ElementOf<string[]>; // string八、泛型工具函数模式
8.1 函数式编程
typescript
// map: T[] -> U[]
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
// filter: T[] -> T[]
function filter<T>(arr: T[], predicate: (item: T) => boolean): T[] {
return arr.filter(predicate);
}
// reduce: T[] -> U
function reduce<T, U>(arr: T[], fn: (acc: U, item: T) => U, init: U): U {
return arr.reduce(fn, init);
}8.2 异步函数
typescript
async function fetchData<T>(url: string): Promise<T> {
const res = await fetch(url);
return res.json();
}
// 使用
interface User { id: number; name: string }
const user = await fetchData<User>('/api/user/1');
user.id; // ✅ 类型安全九、泛型 vs any vs unknown
typescript
// ❌ any: 完全放弃类型检查
function fn1(value: any): any {
return value;
}
const a = fn1(123);
a.foo.bar.baz; // 编译过,运行不知道
// ❌ unknown: 类型安全但每次都要断言
function fn2(value: unknown): unknown {
return value;
}
const b = fn2(123);
b.toFixed(); // ❌ Object is of type 'unknown'
// ✅ 泛型: 保留类型信息
function fn3<T>(value: T): T {
return value;
}
const c = fn3(123);
c.toFixed(); // ✅ number 类型有 toFixed| 特性 | any | unknown | 泛型 <T> |
|---|---|---|---|
| 类型检查 | ❌ 关闭 | ✅ 严格 | ✅ 保留 |
| 调用方类型 | 丢失 | 丢失 | 推断/指定 |
| 灵活性 | 最高 | 中 | 高 |
| 何时用 | 迁移期兜底 | 不确定类型 | 首选 |
十、常见错误
10.1 泛型参数数量不匹配
typescript
function pair<T, U>(a: T, b: U): [T, U] {
return [a, b];
}
pair<string>('a', 'b'); // ❌ Expected 2 type arguments, but got 1
pair<string, number>('a', 1); // ✅10.2 约束太宽
typescript
// ❌ 约束等于没约束
function process<T extends object>(obj: T) { }
// ✅ 更具体的约束
function process<T extends { id: number }>(obj: T) {
console.log(obj.id); // ✅ 一定有 id
}10.3 忘记类型推断
typescript
// ❌ 写死类型,失去灵活性
function identity(value: any): any {
return value;
}
// ✅ 泛型让调用方决定类型
function identity<T>(value: T): T {
return value;
}十一、实战案例
11.1 通用分页响应
typescript
interface PageParams {
page: number;
pageSize: number;
}
interface PageResult<T> {
items: T[];
total: number;
page: number;
pageSize: number;
}
async function fetchPage<T>(url: string, params: PageParams): Promise<PageResult<T>> {
const query = `?page=${params.page}&pageSize=${params.pageSize}`;
const res = await fetch(url + query);
return res.json();
}
// 使用
const userPage = await fetchPage<User>('/api/users', { page: 1, pageSize: 10 });
userPage.items[0].name; // ✅ 类型安全11.2 通用缓存
typescript
class Cache<K, V> {
private store = new Map<K, V>();
set(key: K, value: V): void {
this.store.set(key, value);
}
get(key: K): V | undefined {
return this.store.get(key);
}
has(key: K): boolean {
return this.store.has(key);
}
}
const userCache = new Cache<number, User>();
userCache.set(1, { id: 1, name: 'Tom' });11.3 通用选择器函数
typescript
// 安全地访问嵌套字段
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
const users = [
{ name: 'Tom', age: 18 },
{ name: 'Jerry', age: 20 }
];
const names = pluck(users, 'name'); // string[]
const ages = pluck(users, 'age'); // number[]十二、本章小结
| 要点 | 关键 |
|---|---|
| 泛型语法 | <T> 类型参数化 |
| 泛型函数 | function fn<T>(arg: T): T {} |
| 泛型接口 | interface Box<T> { value: T } |
| 泛型约束 | <T extends Constraint> 限制类型范围 |
| keyof 约束 | <K extends keyof T> 约束键名 |
| 默认值 | <T = string> |
| vs any | 泛型保留类型,any 丢失 |
动手练习
- 基础泛型:实现一个
last<T>(arr: T[]): T | undefined函数 - 泛型约束:写一个
sum<T extends number>(arr: T[]): T函数 - 泛型类:实现一个
Queue<T>队列类,支持enqueue/dequeue/front - keyof 约束:写一个
setProp<T, K extends keyof T>(obj: T, key: K, value: T[K])函数
推荐阅读
- 📖 TypeScript Handbook - Generics — 官方泛型文档
- 📖 TypeScript Generics Tutorial — 泛型约束
- 🌐 Type-Level TypeScript — 类型级编程进阶