泛型
泛型的本质
泛型是 TypeScript 类型系统中实现"参数化类型"的机制。它允许在定义函数、接口或类型时不预先指定具体类型,而是在使用时由调用者决定。泛型的核心价值在于在保持类型安全的前提下实现代码复用。
面试定位:泛型是区分 TypeScript 初级与中级开发者的分水岭。面试官关注的不仅是语法,更是候选人能否设计出良好约束的泛型 API——既灵活又不过度抽象。
泛型函数
基础语法与类型推断
// 基础泛型函数
function identity<T>(value: T): T {
return value;
}
// TypeScript 自动推断类型参数
const str = identity("hello"); // T 推断为 "hello"(字面量类型)
const num = identity(42); // T 推断为 42
// 显式指定类型参数(通常不必要,但在推断不足时有用)
const val = identity<string>("hello"); // T 为 string
多个类型参数
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
const result = map([1, 2, 3], (n) => n.toString());
// T 推断为 number, U 推断为 string
泛型箭头函数
在 TSX 文件中,泛型箭头函数的 <T> 会被解析为 JSX 标签,需要额外处理:
// 方式一:添加 extends 约束
const identity = <T extends unknown>(value: T): T => value;
// 方式二:添加尾逗号(仅在某些解析器中有效)
const identity = <T,>(value: T): T => value;
泛型约束(Generic Constraints)
extends 关键字
约束限定类型参数必须满足某种形状,是泛型 API 设计的核心工具:
// 约束:T 必须有 length 属性
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
getLength("hello"); // ✅ string 有 length
getLength([1, 2, 3]); // ✅ 数组有 length
getLength(42); // ❌ number 没有 length
keyof 约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
getProperty(user, "name"); // 返回类型为 string
getProperty(user, "age"); // 返回类型为 number
getProperty(user, "email"); // ❌ 编译错误
多重约束
interface Serializable { serialize(): string }
interface Loggable { log(): void }
function process<T extends Serializable & Loggable>(item: T) {
item.serialize();
item.log();
}
泛型接口与泛型类型别名
泛型接口
interface Repository<T> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
create(entity: Omit<T, "id">): Promise<T>;
update(id: string, partial: Partial<T>): Promise<T>;
delete(id: string): Promise<void>;
}
// 使用
interface User { id: string; name: string; email: string }
class UserRepository implements Repository<User> {
// 实现所有方法,类型完全确定
}
泛型类型别名
// 通用 API 响应包装
type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; error: string };
// 分页包装
type Paginated<T> = {
items: T[];
total: number;
page: number;
pageSize: number;
hasNext: boolean;
};
// 组合使用
type UserListResponse = ApiResponse<Paginated<User>>;
泛型默认值
// 默认类型参数
interface EventEmitter<Events extends Record<string, unknown> = Record<string, unknown>> {
on<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void): void;
emit<K extends keyof Events>(event: K, payload: Events[K]): void;
}
// 可以不指定类型参数
const emitter: EventEmitter = createEmitter();
// 也可以显式指定,获得更精确的类型
interface AppEvents {
login: { userId: string };
logout: undefined;
}
const typedEmitter: EventEmitter<AppEvents> = createEmitter();
常见泛型模式
工厂函数模式
function createStore<State>(initialState: State) {
let state = initialState;
return {
getState: (): State => state,
setState: (newState: Partial<State>) => {
state = { ...state, ...newState };
},
};
}
const store = createStore({ count: 0, name: "app" });
store.setState({ count: 1 }); // ✅ 类型安全
store.setState({ invalid: true }); // ❌
Builder 模式
class QueryBuilder<T> {
private filters: Array<(item: T) => boolean> = [];
where<K extends keyof T>(key: K, value: T[K]): this {
this.filters.push((item) => item[key] === value);
return this;
}
select<K extends keyof T>(...keys: K[]): Pick<T, K>[] {
// 实现略
return [] as Pick<T, K>[];
}
}
类型安全的事件系统
type EventMap = {
click: { x: number; y: number };
keydown: { key: string; code: string };
resize: { width: number; height: number };
};
function on<K extends keyof EventMap>(
event: K,
handler: (payload: EventMap[K]) => void
): void {
// 实现略
}
on("click", (payload) => {
console.log(payload.x, payload.y); // 类型完全推断
});
泛型与条件类型的结合
泛型与条件类型结合时,会触发"分布式条件类型"行为:
// 分布式条件类型
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<string | number>; // true | false → boolean
// 利用分布式特性实现过滤
type Filter<T, U> = T extends U ? T : never;
type StringOrNumber = Filter<string | number | boolean, string | number>;
// string | number
泛型设计原则
最少类型参数原则
每个类型参数都应该在至少两个位置使用(参数和返回值之间建立关联)。如果一个类型参数只在一个位置出现,很可能不需要它:
// ❌ T 只在一个位置出现,没有建立关联
function logValue<T>(value: T): void {
console.log(value);
}
// ✅ 直接使用 unknown
function logValue(value: unknown): void {
console.log(value);
}
推断优先于显式标注
设计泛型 API 时,应让 TypeScript 能从参数自动推断类型,而非要求调用者手动指定:
// ❌ 调用者必须显式指定
function parse<T>(json: string): T {
return JSON.parse(json);
}
const user = parse<User>(jsonStr); // 实际上没有运行时校验
// ✅ 通过 schema 参数让类型自动推断
function parse<T>(json: string, schema: ZodSchema<T>): T {
return schema.parse(JSON.parse(json));
}
const user = parse(jsonStr, userSchema); // T 从 schema 推断
约束越精确越好
// ❌ 过于宽泛
function merge<T>(a: T, b: T): T { /* ... */ }
// ✅ 约束为对象类型
function merge<T extends Record<string, unknown>>(a: T, b: Partial<T>): T {
return { ...a, ...b };
}
面试高频问题
Q: 什么时候该使用泛型?什么时候过度设计了?
回答要点:泛型适用于需要在多种类型上复用同一逻辑,且需要保留输入与输出之间类型关系的场景。如果函数只在一种具体类型上使用、或者类型参数只在签名的一个位置出现,那么泛型就是过度设计。良好的泛型 API 应该让调用者几乎感知不到泛型的存在——类型应该能被自动推断。
Q: extends 在泛型中的含义与在 interface 中有何不同?
回答要点:在 interface 中,extends 表示继承关系,子接口获得父接口的所有属性。在泛型约束中,extends 表示"可赋值给"关系——T extends U 意味着 T 必须是 U 的子类型。在条件类型中,extends 则用于类型分支判断。三种用法共享"子类型兼容"的语义内核,但应用场景完全不同。
Q: 如何设计一个类型安全的通用 API 请求函数?
回答要点:通过泛型参数化响应类型,并结合运行时校验(如 Zod schema)确保类型断言有运行时保障。关键是避免 as T 这种纯编译时断言,而是通过 schema 验证让泛型参数同时约束编译时类型和运行时行为。