跳到主要内容
预计阅读 30 分钟

泛型编程:编写一次适配多种数据类型

需求场景

订单系统中有大量结构相似但数据类型不同的逻辑:

  • 商品列表需要分页查询,返回 Product[]
  • 订单列表也需要分页查询,返回 Order[]
  • 用户列表同样需要分页查询,返回 Customer[]

三个查询函数的内部逻辑几乎一样——接收页码和页容量,返回数据列表和分页元信息。你不想为每种实体写一遍重复的代码。

但如果用 any 来统一数据类型,就丧失了类型安全。传入 Product[] 返回的却是 any[],后续对返回数据的操作完全没有编译器保护。

泛型(Generics)就是为了解决这个矛盾而设计的:定义时不确定具体类型,使用时再指定,全程保持类型安全。

从问题出发理解泛型

问题复现

假设需要一个函数,接收一个值并原样返回。用 any 实现:

function echo(input: any): any {
  return input;
}

const result = echo("order-001");
// result 的类型是 any,不是 string
result.nonExistent(); // 编译器不报错,运行时崩溃

类型信息在 any 这里断裂了。

用函数重载呢?每增加一种类型就要多写一个签名,完全不可维护:

function echo(input: string): string;
function echo(input: number): number;
function echo(input: boolean): boolean;
// 无穷无尽...

泛型的解法

泛型用类型参数解决这个问题。类型参数用尖括号 <> 声明,它是一个占位符,在调用时被替换为具体的类型:

function echo<T>(input: T): T {
  return input;
}

// 显式指定类型参数
const orderId = echo<string>("order-001");  // orderId: string
const qty = echo<number>(5);                // qty: number

// 让编译器自动推断
const flag = echo(true);                    // flag: boolean

T 是类型参数的名称,调用 echo<string>("order-001") 时,函数签名变为 (input: string): string。类型信息完整传递,编译器对返回值的后续操作都有保护。

常见的类型参数命名约定:

名称语义
T通用类型
U, V第二、第三类型参数
K键类型
V值类型
E元素类型
R返回值类型

泛型函数

单类型参数

function fillArray<E>(count: number, filler: E): E[] {
  const output: E[] = [];
  for (let i = 0; i < count; i++) {
    output.push(filler);
  }
  return output;
}

const tags = fillArray<string>(3, "new");   // string[]
const zeros = fillArray(5, 0);             // number[](自动推断)

多类型参数

function pairSwap<A, B>(pair: [A, B]): [B, A] {
  return [pair[1], pair[0]];
}

const swapped = pairSwap(["sku-100", 29.9]); // [number, string]

一个更实用的例子——映射转换函数:

function transform<TIn, TOut>(items: TIn[], converter: (item: TIn) => TOut): TOut[] {
  return items.map(converter);
}

const prices = [10, 20, 30];
const labels = transform(prices, p => `$${p.toFixed(2)}`);
// labels: string[],值为 ["$10.00", "$20.00", "$30.00"]
// TIn 推断为 number,TOut 推断为 string

泛型函数变量

泛型函数赋值给变量的两种写法:

let myEcho: <T>(input: T) => T = echo;
let myEcho2: { <T>(input: T): T } = echo;

泛型接口

接口的类型参数有两种作用域。

接口级类型参数

类型参数声明在接口名后,所有成员共享同一个类型。使用时必须指定类型:

interface DataStore<T> {
  current: T;
  history: T[];
  update(newVal: T): void;
  rollback(): T;
}

const priceStore: DataStore<number> = {
  current: 99,
  history: [89, 95],
  update(newVal) {
    this.history.push(this.current);
    this.current = newVal;
  },
  rollback() {
    const prev = this.history.pop();
    if (prev !== undefined) this.current = prev;
    return this.current;
  },
};

方法级类型参数

类型参数声明在方法上,每次调用可以使用不同的类型:

interface Converter {
  <TIn, TOut>(input: TIn): TOut;
}

实际案例:数据访问层

interface CrudRepository<T> {
  getById(id: number): T | undefined;
  getAll(): T[];
  add(item: T): T;
  modify(id: number, partial: Partial<T>): T;
  remove(id: number): boolean;
}

泛型类

泛型类在数据容器、集合类的设计中常见。

class Queue<E> {
  private items: E[] = [];

  enqueue(item: E): void {
    this.items.push(item);
  }

  dequeue(): E | undefined {
    return this.items.shift();
  }

  front(): E | undefined {
    return this.items[0];
  }

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

  isEmpty(): boolean {
    return this.items.length === 0;
  }
}

const orderQueue = new Queue<string>();
orderQueue.enqueue("ORD-001");
orderQueue.enqueue("ORD-002");
console.log(orderQueue.dequeue()); // "ORD-001"

const priorityQueue = new Queue<{ task: string; level: number }>();
priorityQueue.enqueue({ task: "process-payment", level: 1 });

多类型参数的泛型类:

class KeyValueEntry<K, V> {
  constructor(public label: K, public payload: V) {}

  describe(): string {
    return `${this.label}: ${this.payload}`;
  }
}

const entry = new KeyValueEntry<string, number>("quantity", 42);

静态成员无法访问类级别的类型参数,原因在于静态成员挂载在构造函数上,而泛型参数只在实例化时才被确定:

class Container<T> {
  // static defaultItem: T; // 编译错误
  static instanceCount: number = 0; // 普通静态成员没问题

  constructor(public content: T) {
    Container.instanceCount++;
  }
}

泛型类在继承时需要传递或指定类型参数:

// 传递类型参数
class LabeledQueue<E> extends Queue<E> {
  constructor(public queueName: string) {
    super();
  }
}

// 指定具体类型
class NumberQueue extends Queue<number> {
  total(): number {
    let sum = 0;
    while (!this.isEmpty()) {
      sum += this.dequeue()!;
    }
    return sum;
  }
}

泛型约束

不加约束的类型参数 T 相当于 unknown,无法做任何假设。当需要类型参数满足某些条件时,使用 extends 约束。

结构约束

function printLength<T extends { length: number }>(target: T): void {
  console.log(`Length: ${target.length}`);
}

printLength("hello");         // string 有 length
printLength([1, 2, 3]);       // 数组有 length
printLength({ length: 10 });  // 对象有 length 属性
// printLength(42);           // 编译错误:number 没有 length

用接口定义约束更清晰:

interface Measurable {
  length: number;
}

function longer<T extends Measurable>(x: T, y: T): T {
  return x.length >= y.length ? x : y;
}

const result = longer("typescript", "js"); // 返回 "typescript",类型为 string

泛型约束的精妙之处:返回类型是 T 而非 Measurable,返回值保留了传入的具体类型。

类型参数之间的约束

function getField<TObj, TKey extends keyof TObj>(obj: TObj, key: TKey): TObj[TKey] {
  return obj[key];
}

const product = { sku: "P-100", price: 299, stock: 50 };
const sku = getField(product, "sku");     // string
const price = getField(product, "price"); // number
// getField(product, "color");            // 编译错误:"color" 不在键名中

TKey extends keyof TObj 约束 TKey 必须是 TObj 的键名之一,编译器阻止访问不存在的属性。

泛型默认值

类型参数可以设置默认类型:

interface ApiResult<TData = unknown> {
  code: number;
  msg: string;
  data: TData;
}

// 不指定类型参数时使用默认值
const generic: ApiResult = { code: 200, msg: "ok", data: "anything" };

// 指定具体类型
interface OrderBrief { orderId: string; total: number }
const typed: ApiResult<OrderBrief[]> = {
  code: 200,
  msg: "ok",
  data: [{ orderId: "ORD-001", total: 599 }],
};

有默认值的类型参数必须放在无默认值的参数后面:

// 合法
interface Mapping<K, V = string> {
  key: K;
  value: V;
}

// 非法
// interface Mapping<K = string, V> {} // 编译错误

泛型与数组

TypeScript 中 number[] 实际上是 Array<number> 的语法糖:

let nums1: number[] = [1, 2, 3];
let nums2: Array<number> = [1, 2, 3]; // 完全等价

// 复杂类型时泛型写法更清晰
let pending: Array<Promise<string>> = [
  fetch("/api/name").then(r => r.text()),
  Promise.resolve("fallback"),
];

ReadonlyArray<T> 表示不可修改的数组:

const frozen: ReadonlyArray<number> = [10, 20, 30];
// frozen.push(40);  // 编译错误
// frozen[0] = 99;   // 编译错误

泛型类型别名

type Nullable<T> = T | null | undefined;

type Tagged<T> = {
  tag: string;
  data: T;
};

type BinaryTree<T> = {
  value: T;
  left: BinaryTree<T> | null;
  right: BinaryTree<T> | null;
};

const tree: BinaryTree<number> = {
  value: 10,
  left: { value: 5, left: null, right: null },
  right: { value: 15, left: null, right: null },
};

常用泛型模式

Promise

function loadOrder(orderId: string): Promise<OrderBrief> {
  return fetch(`/api/orders/${orderId}`).then(r => r.json());
}

async function main() {
  const order = await loadOrder("ORD-001"); // order: OrderBrief
  console.log(order.total);
}

数组高阶方法

const orderAmounts = [100, 200, 300];

const formatted = orderAmounts.map<string>(amt => `CNY ${amt}`);
const bigOrders = orderAmounts.filter(amt => amt >= 200);
const grandTotal = orderAmounts.reduce<number>((acc, cur) => acc + cur, 0);

Record<K, V>

type PermissionMap = Record<string, boolean>;

const permissions: PermissionMap = {
  canEdit: true,
  canDelete: false,
  canView: true,
};

type DayOfWeek = "mon" | "tue" | "wed" | "thu" | "fri";
type WeeklyPlan = Record<DayOfWeek, string>;

const plan: WeeklyPlan = {
  mon: "Sprint planning",
  tue: "Development",
  wed: "Code review",
  thu: "Development",
  fri: "Retrospective",
};

Partial 与 Required

interface OrderDraft {
  customerId: string;
  items: string[];
  note: string;
}

// 所有字段变可选——适合增量更新
function modifyDraft(id: string, changes: Partial<OrderDraft>): void {
  console.log(`Updating draft ${id}`, changes);
}

modifyDraft("D-001", { note: "Updated note" }); // 不需要传所有字段

泛型工厂函数

function instantiate<T>(
  Blueprint: new (...args: any[]) => T,
  ...args: any[]
): T {
  return new Blueprint(...args);
}

class Warehouse {
  constructor(public code: string, public city: string) {}
}

class Truck {
  constructor(public plateNo: string) {}
}

const wh = instantiate(Warehouse, "WH-01", "Shanghai");
const truck = instantiate(Truck, "沪A12345");

泛型使用原则

1. 类型参数至少使用两次

泛型的价值在于建立类型之间的关联。如果类型参数只出现一次,泛型没有意义:

// 无意义:T 只出现一次
function alert<T extends string>(msg: T): void {
  console.log(msg);
}

// 有意义:T 在输入和输出之间建立关联
function firstOf<T>(items: T[]): T {
  return items[0];
}

2. 控制类型参数的数量

// 过多参数降低可读性
function doWork<A, B, C, D>(a: A, b: B, c: C): D { /* ... */ }

// 保持简洁
function convert<TSource, TTarget>(source: TSource): TTarget { /* ... */ }

3. 优先利用类型推断

const doubled = [1, 2, 3].map(n => n * 2); // 自动推断,不需要手动指定

综合实战:类型安全的消息总线

interface MessageSchema {
  "order:created": { orderId: string; amount: number };
  "order:cancelled": { orderId: string; reason: string };
  "stock:updated": { sku: string; delta: number };
}

class MessageBus<TSchema extends Record<string, any>> {
  private listeners = new Map<keyof TSchema, Array<(payload: any) => void>>();

  subscribe<TEvent extends keyof TSchema>(
    event: TEvent,
    handler: (payload: TSchema[TEvent]) => void
  ): void {
    const handlers = this.listeners.get(event) || [];
    handlers.push(handler);
    this.listeners.set(event, handlers);
  }

  publish<TEvent extends keyof TSchema>(
    event: TEvent,
    payload: TSchema[TEvent]
  ): void {
    const handlers = this.listeners.get(event);
    if (handlers) {
      handlers.forEach(fn => fn(payload));
    }
  }

  unsubscribe<TEvent extends keyof TSchema>(
    event: TEvent,
    handler: (payload: TSchema[TEvent]) => void
  ): void {
    const handlers = this.listeners.get(event);
    if (handlers) {
      this.listeners.set(event, handlers.filter(fn => fn !== handler));
    }
  }
}

const bus = new MessageBus<MessageSchema>();

bus.subscribe("order:created", (payload) => {
  // payload 自动推断为 { orderId: string; amount: number }
  console.log(`New order ${payload.orderId} for ${payload.amount}`);
});

bus.subscribe("stock:updated", (payload) => {
  // payload 自动推断为 { sku: string; delta: number }
  console.log(`Stock ${payload.sku} changed by ${payload.delta}`);
});

bus.publish("order:created", { orderId: "ORD-500", amount: 1299 });
// bus.publish("order:created", { orderId: 123 }); // 编译错误:orderId 应为 string
// bus.publish("unknown:event", {});               // 编译错误:事件名不在 schema 中

本章回顾

概念说明关键语法
泛型基础类型参数化,定义时不确定,使用时指定function fn<T>(arg: T): T
多类型参数函数可以有多个类型参数<A, B>
泛型函数参数和返回值使用类型参数function swap<A, B>(pair: [A, B])
泛型接口接口使用类型参数interface Store<T> { current: T }
泛型类类使用类型参数class Queue<E> { ... }
泛型约束限制类型参数的结构<T extends { length: number }>
keyof 约束约束为对象的键名<K extends keyof T>
泛型默认值类型参数的默认类型<T = string>
Array数组的泛型表示Array<number> 等价 number[]
Promise异步结果的泛型类型Promise<Order>
工具类型内置泛型工具Partial<T> Record<K, V>
使用原则类型参数至少用两次,不要过度使用优先利用自动推断

购买课程解锁全部内容

告别类型错误:12 章掌握 TypeScript 工程实战

¥29.90