Skip to content

泛型编程完整体系

一句话概述:泛型约束、默认值、const 类型参数、高阶泛型模式——用泛型写出类型安全且可复用的代码

什么是泛型?

定义:泛型(Generics)是参数化类型,允许在定义函数、类、接口时不预先指定具体类型,而是在使用时由调用方或编译器推断确定。

涉及场景

  • 通用数据结构Array<T>Map<K, V>Promise<T>
  • 工具函数pickomitmerge 等通用操作
  • React 组件:泛型 Props、泛型 Hooks
  • API 层封装:类型安全的请求函数

作用

  1. 在保持类型安全的同时实现代码复用
  2. 让编译器自动推断具体类型,减少手动标注
  3. 约束输入输出之间的类型关系

一、泛型基础

泛型函数

typescript
// 没有泛型:丢失类型信息
function identity(arg: any): any {
  return arg;
}

// 有泛型:保持类型安全
function identity<T>(arg: T): T {
  return arg;
}

// 调用方式
identity<string>('hello'); // 显式指定
identity('hello');          // 自动推断(推荐)
identity(42);               // T = number

泛型接口

typescript
interface Repository<T> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  create(data: Omit<T, 'id'>): Promise<T>;
  update(id: string, data: Partial<T>): Promise<T>;
  delete(id: string): Promise<void>;
}

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

// 使用时指定具体类型
const userRepo: Repository<User> = {
  async findById(id) { /* ... */ return null; },
  async findAll() { /* ... */ return []; },
  async create(data) { /* ... */ return { id: '1', ...data }; },
  async update(id, data) { /* ... */ return {} as User; },
  async delete(id) { /* ... */ },
};

泛型类型别名

typescript
// 通用响应类型
type ApiResponse<T> = {
  code: number;
  data: T;
  message: string;
  timestamp: number;
};

// 分页响应
type PaginatedResponse<T> = ApiResponse<{
  items: T[];
  total: number;
  page: number;
  pageSize: number;
}>;

// 使用
type UserListResponse = PaginatedResponse<User>;
// { code: number; data: { items: User[]; total: number; ... }; ... }

二、泛型约束

extends 约束

typescript
// 约束 T 必须有 length 属性
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest('hello', 'hi');       // ✅ string 有 length
longest([1, 2], [1, 2, 3]);   // ✅ 数组有 length
// longest(10, 20);            // ❌ number 没有 length

keyof 约束

typescript
// 约束 K 必须是 T 的键
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' };

getProperty(user, 'name');  // string
getProperty(user, 'age');   // number
// getProperty(user, 'foo'); // ❌ "foo" 不是 keyof User

// 安全的 pick 函数
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const result = {} as Pick<T, K>;
  keys.forEach(key => { result[key] = obj[key]; });
  return result;
}

const partial = pick(user, ['name', 'email']);
// { name: string; email: string }

多重约束

typescript
// 同时满足多个约束
interface Serializable {
  serialize(): string;
}

interface Loggable {
  log(): void;
}

function process<T extends Serializable & Loggable>(item: T): string {
  item.log();
  return item.serialize();
}

约束中使用类型参数

typescript
// K 约束依赖于 T
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
  obj[key] = value;
}

const user = { name: 'Alice', age: 25 };
setProperty(user, 'name', 'Bob');  // ✅
// setProperty(user, 'name', 42);  // ❌ number 不能赋给 string
// setProperty(user, 'age', 'old'); // ❌ string 不能赋给 number

三、泛型默认值

基本语法

typescript
// 默认值让泛型参数可选
interface ApiResponse<T = unknown> {
  code: number;
  data: T;
  message: string;
}

// 不指定时使用默认值
const res1: ApiResponse = { code: 200, data: 'anything', message: 'ok' };
// data: unknown

// 指定时覆盖默认值
const res2: ApiResponse<User> = { code: 200, data: user, message: 'ok' };
// data: User

默认值 + 约束

typescript
// 默认值必须满足约束
interface Collection<T extends object = Record<string, unknown>> {
  items: T[];
  count: number;
}

const c1: Collection = { items: [{ a: 1 }], count: 1 };
// T = Record<string, unknown>

const c2: Collection<User> = { items: [user], count: 1 };
// T = User

多个泛型参数的默认值顺序

typescript
// 有默认值的参数必须在没有默认值的参数后面
interface Mapper<Input, Output = Input> {
  map(input: Input): Output;
}

const identity: Mapper<string> = {
  map: (input) => input, // Output 默认为 string
};

const toNumber: Mapper<string, number> = {
  map: (input) => parseInt(input),
};

四、const 类型参数(TS 5.0+)

问题:泛型推断会拓宽类型

typescript
function createConfig<T extends Record<string, string>>(config: T): T {
  return config;
}

const config = createConfig({
  home: '/',
  about: '/about',
});
// T = { home: string; about: string } ❌ 字面量信息丢失

解决:<const T> 自动推断为字面量类型

typescript
function createConfig<const T extends Record<string, string>>(config: T): T {
  return config;
}

const config = createConfig({
  home: '/',
  about: '/about',
});
// T = { readonly home: "/"; readonly about: "/about" } ✅ 保留字面量

// 等价于调用方写 as const,但对调用方更友好

实际应用

typescript
// 1. 类型安全的路由定义
function defineRoutes<const T extends readonly { path: string; name: string }[]>(
  routes: T
) {
  return routes;
}

const routes = defineRoutes([
  { path: '/', name: 'home' },
  { path: '/about', name: 'about' },
]);
// routes[0].name 的类型是 "home" 而非 string

type RouteName = (typeof routes)[number]['name'];
// "home" | "about"

// 2. 事件系统
function defineEvents<const T extends Record<string, (...args: any[]) => void>>(
  events: T
) {
  return events;
}

const events = defineEvents({
  click: (x: number, y: number) => {},
  resize: (width: number, height: number) => {},
});

// 3. 状态机
function createMachine<const T extends {
  initial: string;
  states: Record<string, { on?: Record<string, string> }>;
}>(config: T) {
  return config;
}

const machine = createMachine({
  initial: 'idle',
  states: {
    idle: { on: { START: 'running' } },
    running: { on: { STOP: 'idle' } },
  },
});
// machine.initial 的类型是 "idle" 而非 string

五、高阶泛型模式

泛型工厂函数

typescript
// 创建类型安全的工厂
function createFactory<T>(Constructor: new (...args: any[]) => T) {
  return {
    create(...args: ConstructorParameters<typeof Constructor>): T {
      return new Constructor(...args);
    },
  };
}

class User {
  constructor(public name: string, public age: number) {}
}

const userFactory = createFactory(User);
const user = userFactory.create('Alice', 25); // User 类型

函数重载 vs 泛型

typescript
// ❌ 重载:每种组合都要写一遍
function parse(input: string): string;
function parse(input: number): number;
function parse(input: string | number) {
  return input;
}

// ✅ 泛型:一次定义覆盖所有
function parse<T extends string | number>(input: T): T {
  return input;
}

Builder 模式(链式调用保持类型)

typescript
class QueryBuilder<T extends object, Selected extends keyof T = never> {
  private selectedFields: (keyof T)[] = [];
  private conditions: string[] = [];

  select<K extends keyof T>(...fields: K[]): QueryBuilder<T, Selected | K> {
    this.selectedFields.push(...fields);
    return this as any;
  }

  where(condition: string): this {
    this.conditions.push(condition);
    return this;
  }

  execute(): Promise<Pick<T, Selected>[]> {
    // 实际执行查询
    return Promise.resolve([]);
  }
}

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

// 链式调用,类型逐步收窄
const result = await new QueryBuilder<User>()
  .select('name', 'email')
  .where('age > 18')
  .execute();
// result: Pick<User, "name" | "email">[]
// 即 { name: string; email: string }[]

泛型与 React

typescript
// 泛型组件
interface SelectProps<T> {
  options: T[];
  value: T;
  onChange: (value: T) => void;
  getLabel: (option: T) => string;
  getValue: (option: T) => string | number;
}

function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
  return (
    <select
      value={String(getValue(value))}
      onChange={(e) => {
        const selected = options.find(o => String(getValue(o)) === e.target.value);
        if (selected) onChange(selected);
      }}
    >
      {options.map(option => (
        <option key={String(getValue(option))} value={String(getValue(option))}>
          {getLabel(option)}
        </option>
      ))}
    </select>
  );
}

// 使用时 T 自动推断
interface Country { code: string; name: string }

<Select<Country>
  options={[{ code: 'CN', name: '中国' }, { code: 'US', name: '美国' }]}
  value={{ code: 'CN', name: '中国' }}
  onChange={(country) => console.log(country.name)} // country: Country
  getLabel={(c) => c.name}
  getValue={(c) => c.code}
/>

// 泛型 Hook
function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  const setStoredValue = (newValue: T | ((prev: T) => T)) => {
    setValue(prev => {
      const resolved = typeof newValue === 'function'
        ? (newValue as (prev: T) => T)(prev)
        : newValue;
      localStorage.setItem(key, JSON.stringify(resolved));
      return resolved;
    });
  };

  return [value, setStoredValue] as const;
}

// T 从 initialValue 推断
const [theme, setTheme] = useLocalStorage('theme', 'dark');
// theme: string, setTheme: (value: string | ((prev: string) => string)) => void

六、NoInfer<T>(TS 5.4+)

问题:多位置推断冲突

typescript
function createState<T>(initial: T, defaultValue: T) {
  return { value: initial, default: defaultValue };
}

// T 从两个位置推断,结果可能不符合预期
createState('hello', 42);
// T 被推断为 string | number,不报错

解决:阻止特定位置参与推断

typescript
function createState<T>(initial: T, defaultValue: NoInfer<T>) {
  return { value: initial, default: defaultValue };
}

// T 只从 initial 推断为 string
createState('hello', 42);
// ❌ Argument of type 'number' is not assignable to parameter of type 'string'

createState('hello', 'world'); // ✅

实际场景

typescript
// 1. 带默认值的配置
function configure<T extends object>(
  schema: T,
  defaults: NoInfer<Partial<T>>
) { /* ... */ }

// T 只从 schema 推断
configure(
  { host: '', port: 0, debug: false },
  { debug: true } // 受 schema 类型约束
);

// 2. 事件系统
function on<T extends string>(
  event: T,
  handler: (data: NoInfer<T>) => void
) { /* ... */ }

// T 只从 event 推断
on('click', (data) => {
  // data: "click"
});

面试高频题

1. 泛型和 any 的区别是什么?

答案any 放弃类型检查,输入和输出之间没有类型关联。泛型保持类型安全,输入类型决定输出类型。例如 function first<T>(arr: T[]): T 保证返回值类型与数组元素类型一致,而 function first(arr: any[]): any 丢失了这个关联。

2. 什么时候用泛型约束?extends 在泛型中的含义?

答案:当需要限制泛型参数的范围时使用约束。T extends U 表示 T 必须是 U 的子类型(结构化兼容),不是面向对象的"继承"。常见模式:T extends object(必须是对象)、K extends keyof T(必须是 T 的键)、T extends { length: number }(必须有 length 属性)。

3. <const T>as const 的区别?

答案as const 是调用方在值后面加的断言,<const T> 是库开发者在泛型定义上加的修饰。效果相同(推断为字面量类型),但 <const T> 对调用方更友好——调用方不需要记住加 as const,类型定义方就能保证推断精度。

4. 泛型在 React 中有哪些典型应用?

答案:泛型列表组件(List<T>)、泛型选择器(Select<T>)、泛型表单(Form<T>)、泛型 Hook(useLocalStorage<T>useFetch<T>)。TS 5.x 支持泛型 forwardRef,解决了之前泛型组件无法转发 ref 的问题。