泛型编程:编写一次适配多种数据类型
需求场景
订单系统中有大量结构相似但数据类型不同的逻辑:
- 商品列表需要分页查询,返回
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