Chapter 04

泛型:函数、类与约束

系统掌握泛型函数、泛型接口、泛型类与类型约束,用泛型构建类型安全的通用代码

泛型的本质:类型级别的函数

从值空间到类型空间的映射

泛型参数 <T> 本质上是一个类型级别的函数参数——它让类型定义可以接受输入类型,产生输出类型。理解这个类比,是掌握复杂泛型(条件类型、infer)的基础。

类型参数(Type Parameter)
尖括号中的大写字母(T、K、V、R 等),是类型的占位符。调用函数或实例化类时,TypeScript 通过类型推断或显式指定来确定具体类型。约定:T 表示通用类型,K 表示键类型,V 表示值类型,R 表示返回类型,E 表示错误类型。
类型推断(Type Inference)
大多数情况下,TypeScript 可以从函数参数值自动推断出类型参数,无需手动指定。如 identity(42),TypeScript 看到参数是 number,自动推断 T = number。只有在推断失败时(如泛型工厂函数无参数),才需要显式 identity<number>
协变与逆变(Covariance / Contravariance)
泛型在不同位置的方差不同:函数返回值位置的类型参数是协变的(子类可以赋给父类位置);函数参数位置是逆变的(父类可以赋给子类位置)。TypeScript 4.7 引入了 out T(协变标记)和 in T(逆变标记)来显式声明方差。
const 类型参数(TypeScript 5.0+)
在泛型函数的类型参数前加 const,让 TypeScript 在推断时保留字面量类型(类似 as const)。如 function pick<const T extends object, ...>,调用时数组/对象参数会保留精确的字面量类型,而不是被拓宽为 string/number 等宽类型。
// TypeScript 5.0+ const 类型参数
function makeArray<const T>(...args: T[]): T[] {
  return args;
}

// 不加 const:类型被拓宽
function makeArrayWide<T>(...args: T[]): T[] { return args; }
const a = makeArrayWide('left', 'right');  // T = string(拓宽)

// 加 const:保留字面量类型
const b = makeArray('left', 'right');      // T = 'left' | 'right'(保留字面量)
// b 的类型是 ('left' | 'right')[],不是 string[]

// 实际应用:路由类型安全
function createRoutes<const T extends string>(...routes: T[]): Set<T> {
  return new Set(routes);
}
const routes = createRoutes('/home', '/about', '/profile');
// routes 类型是 Set<'/home' | '/about' | '/profile'>

泛型基础

为什么需要泛型?

假设你想写一个通用的「栈」(Stack)数据结构,如果不用泛型,你必须为每种类型写一个专用版本(StringStack、NumberStack…),或者用 any 失去类型安全。泛型解决了这个问题——在保持类型安全的同时,让代码具有最大的复用性

// 泛型函数:最简单的例子
// T 是类型参数,调用时确定具体类型
function identity<T>(arg: T): T {
  return arg;
}

// 显式指定类型参数
identity<string>('hello');   // 返回 string
// 类型推断:TypeScript 从参数自动推导 T
identity(42);               // T 推断为 number
identity([1, 2, 3]);        // T 推断为 number[]

泛型栈实现

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.at(-1); }

  get size(): number { return this.items.length; }
}

const numStack = new Stack<number>();
numStack.push(1);
numStack.push('2');  // ❌ 编译错误:Argument of type 'string' is not assignable

const strStack = new Stack<string>();
strStack.push('hello');  // ✅

泛型约束

// 约束:T 必须有 length 属性
function logLength<T extends { length: number }>(arg: T): void {
  console.log(arg.length);
}

logLength('hello');   // ✅ string 有 length
logLength([1, 2, 3]);  // ✅ 数组有 length
logLength(42);         // ❌ number 没有 length

// keyof 约束:确保 key 是对象的键
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: '张三', age: 28, email: 'a@b.com' };
getProperty(user, 'name');   // ✅ 返回 string
getProperty(user, 'age');    // ✅ 返回 number
getProperty(user, 'phone');  // ❌ 'phone' 不是 User 的键

内置工具类型

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

type PartialUser = Partial<User>;          // 所有属性变可选
type RequiredUser = Required<User>;        // 所有属性变必填
type ReadonlyUser = Readonly<User>;        // 所有属性变只读
type PublicUser = Pick<User, 'id' | 'name' | 'email'>; // 只取部分
type SafeUser = Omit<User, 'password'>;   // 去掉密码字段

// Record:创建键值类型的对象
type RoleMap = Record<'admin' | 'user' | 'guest', string[]>;

// 函数相关工具类型
type FetchFn = (url: string, options?: RequestInit) => Promise<Response>;
type FetchParams = Parameters<FetchFn>;    // [string, RequestInit?]
type FetchReturn = ReturnType<FetchFn>;    // Promise<Response>
泛型的实战场景

泛型在实际项目中无处不在:React 的 useState<T>、Promise<T>、Array<T>、事件处理器类型、API 响应类型、数据库查询结果类型……理解泛型是掌握 TypeScript 高级用法的基石。

泛型高级用法

多类型参数

// 多个类型参数的函数
function zip<A, B>(arr1: A[], arr2: B[]): [A, B][] {
  return arr1.map((item, i) => [item, arr2[i]]);
}
zip([1, 2, 3], ['a', 'b', 'c']);  // [number, string][]

// 泛型接口:描述通用 API 响应格式
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: string;
}

// 每个端点返回具体的类型
type UserResponse = ApiResponse<User>;
type ProductListResponse = ApiResponse<Product[]>;

// 泛型工厂函数:创建带类型的 Repository 对象
function createRepository<T extends { id: number }>(data: T[]) {
  return {
    findById(id: number): T | undefined {
      return data.find(item => item.id === id);
    },
    findAll(): T[] {
      return [...data];
    },
    save(item: T): void {
      const index = data.findIndex(i => i.id === item.id);
      if (index >= 0) data[index] = item;
      else data.push(item);
    }
  };
}

const userRepo = createRepository<User>([]);
userRepo.findById(1);   // 返回 User | undefined

默认类型参数

// TypeScript 2.3+ 支持泛型默认值
interface Container<T = string> {
  value: T;
  label: string;
}

// 不提供类型参数时,T 默认为 string
const box: Container = { value: 'hello', label: 'greeting' };
const numBox: Container<number> = { value: 42, label: 'answer' };

// React 组件中的应用(React.FC 默认 Props = {})
type FC<P = {}> = (props: P) => JSX.Element | null;

泛型约束进阶

条件约束与类型推导结合

// 多约束:K 必须同时满足多个条件
function merge<T extends object, U extends object>(a: T, b: U): T & U {
  return { ...a, ...b };  // 返回类型精确是 T & U,不是 object
}

const merged = merge({ name: '张三' }, { age: 28 });
merged.name;  // ✅ string
merged.age;   // ✅ number

// 约束链:一个类型参数约束另一个
function getValue<T, K extends keyof T = keyof T>(obj: T, key?: K):
  K extends keyof T ? T[K] : T[keyof T] {
  return key ? obj[key] : Object.values(obj)[0] as any;
}

// 条件约束:根据类型参数的性质改变返回类型
type Flatten<T> = T extends (infer Item)[] ? Item : T;
type A = Flatten<string[]>;   // string
type B = Flatten<number>;     // number(不是数组,原样返回)
type C = Flatten<boolean[]>;  // boolean
泛型常见误区:过度使用 any 约束

以下写法虽然语法合法,但失去了泛型的意义:function fn<T extends any>(x: T): T——这和 function fn(x: any): any 几乎等价,类型检查形同虚设。约束应精确表达实际需求,如 T extends object(任何对象)或 T extends { id: number }(有 id 属性的对象),而不是 T extends any

工具类型的手动实现

理解工具类型的实现原理有助于创建自定义工具类型:

// Partial:所有属性变可选
type Partial<T> = { [K in keyof T]?: T[K] };

// Required:所有属性变必填(去掉 ?)
type Required<T> = { [K in keyof T]-?: T[K] };

// Readonly:所有属性变只读
type Readonly<T> = { readonly [K in keyof T]: T[K] };

// Pick:从对象类型提取部分属性
type Pick<T, K extends keyof T> = { [P in K]: T[P] };

// Omit:从对象类型排除部分属性
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

// Exclude:从联合类型中排除
type Exclude<T, U> = T extends U ? never : T;
type R = Exclude<'a' | 'b' | 'c', 'a'>;  // 'b' | 'c'

本章小结

本章核心要点