泛型约束与默认值
00:00
泛型约束、默认类型参数与条件泛型
概述
泛型约束和默认值是 TypeScript 泛型系统的重要补充机制。泛型约束(extends)限制类型参数的范围,确保只有满足条件的类型才能作为参数传入。默认类型参数允许在未显式指定类型时使用预设类型,简化泛型的使用。条件泛型结合条件类型和泛型约束,根据类型参数的关系选择不同的类型结果。掌握这些机制能够编写更安全、更灵活的泛型代码。
基础概念
泛型约束(extends):使用 T extends U 语法限制类型参数 T 必须满足类型 U 的结构。编译器会检查传入的类型是否兼容约束。
keyof 约束:K extends keyof T 限制 K 必须是 T 的键类型,常用于类型安全的属性访问。
默认类型参数:使用 T = DefaultType 语法为类型参数提供默认值。调用时可以省略有默认值的参数。
条件泛型:在泛型中使用条件类型 T extends U ? X : Y,根据类型参数的关系返回不同的类型。常用于函数重载的泛型版本。
快速上手
泛型约束
// 约束类型参数必须有 length 属性
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(value: T): void {
console.log(value.length); // 安全访问 length
}
logLength('hello'); // 正确:string 有 length
logLength([1, 2, 3]); // 正确:数组有 length
// logLength(123); // 错误:number 没有 length
// keyof 约束:类型安全的属性访问
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: '张三', age: 30 };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age'); // number
// getProperty(user, 'email'); // 错误:'email' 不是 user 的键
默认类型参数
// 带默认值的泛型接口
interface PaginatedResponse<T, Meta = { total: number; page: number }> {
data: T[];
meta: Meta;
}
// 使用默认 Meta 类型
const response1: PaginatedResponse<User> = {
data: [],
meta: { total: 0, page: 1 },
};
// 自定义 Meta 类型
interface CustomMeta {
total: number;
page: number;
hasNext: boolean;
}
const response2: PaginatedResponse<User, CustomMeta> = {
data: [],
meta: { total: 0, page: 1, hasNext: false },
};
条件泛型
// 根据类型参数选择不同的返回类型
type ApiResponse<T, E = string> = { success: true; data: T } | { success: false; error: E };
// 事件处理器类型:无参数时返回 () => void
type EventHandler<T> = T extends undefined ? () => void : (payload: T) => void;
type ClickHandler = EventHandler<undefined>; // () => void
type InputHandler = EventHandler<{ value: string }>; // (payload: { value: string }) => void
详细用法
多重约束
// 使用交叉类型实现多重约束
interface HasId {
id: string | number;
}
interface HasTimestamp {
createdAt: Date;
updatedAt: Date;
}
// T 必须同时满足 HasId 和 HasTimestamp
function updateEntity<T extends HasId & HasTimestamp>(entity: T, updates: Partial<T>): T {
return {
...entity,
...updates,
updatedAt: new Date() as any,
};
}
// 使用
const article = { id: 1, title: '标题', createdAt: new Date(), updatedAt: new Date() };
updateEntity(article, { title: '新标题' }); // 正确
泛型工厂函数
// 类型安全的工厂函数
function createFactory<T>() {
return {
create(props: T): T {
return { ...props };
},
update(entity: T, props: Partial<T>): T {
return { ...entity, ...props };
},
validate(entity: T, schema: { [K in keyof T]?: (value: T[K]) => boolean }): boolean {
return Object.entries(schema).every(([key, validator]) =>
validator ? validator(entity[key as keyof T]) : true
);
},
};
}
interface Product {
name: string;
price: number;
inStock: boolean;
}
const productFactory = createFactory<Product>();
const product = productFactory.create({ name: '商品', price: 99, inStock: true });
const updated = productFactory.update(product, { price: 79 });
递归泛型约束
// 深度部分类型
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
// 深度更新函数
function deepUpdate<T extends object>(target: T, updates: DeepPartial<T>): T {
const result = { ...target };
for (const key in updates) {
const updateValue = updates[key];
const targetValue = target[key];
if (
typeof updateValue === 'object' &&
updateValue !== null &&
typeof targetValue === 'object' &&
targetValue !== null
) {
result[key] = deepUpdate(targetValue, updateValue);
} else if (updateValue !== undefined) {
result[key] = updateValue as T[Extract<keyof T, string>];
}
}
return result;
}
// 使用
const config = {
api: { baseURL: 'https://api.example.com', timeout: 5000 },
features: { darkMode: true, analytics: false },
};
const updated = deepUpdate(config, {
api: { timeout: 10000 },
});
条件类型与泛型约束
// 根据输入类型推断输出类型
function processValue<T extends string | number>(
value: T
): T extends string ? { text: string; length: number } : { value: number; squared: number } {
if (typeof value === 'string') {
return { text: value, length: value.length } as any;
}
return { value: value as number, squared: (value as number) ** 2 } as any;
}
const stringResult = processValue('hello');
// { text: string; length: number }
const numberResult = processValue(5);
// { value: number; squared: number }
常见场景
类型安全的 EventEmitter
interface EventMap {
click: { x: number; y: number };
change: { value: string };
submit: undefined;
}
class TypedEventEmitter<T extends Record<string, any>> {
private listeners = new Map<keyof T, Set<Function>>();
on<K extends keyof T>(
event: K,
handler: T[K] extends undefined ? () => void : (payload: T[K]) => void
): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(handler);
return () => this.listeners.get(event)?.delete(handler);
}
emit<K extends keyof T>(event: K, ...args: T[K] extends undefined ? [] : [T[K]]): void {
this.listeners.get(event)?.forEach((fn) => fn(...args));
}
}
const emitter = new TypedEventEmitter<EventMap>();
emitter.on('click', ({ x, y }) => console.log(x, y)); // 正确
emitter.on('submit', () => console.log('submitted')); // 正确
emitter.emit('change', { value: 'new' }); // 正确
类型安全的 API 客户端
interface Endpoints {
'/users': {
GET: { response: User[]; query?: { page: number } };
POST: { response: User; body: Omit<User, 'id'> };
};
'/users/:id': {
GET: { response: User; params: { id: string } };
};
}
async function api<Path extends keyof Endpoints, Method extends keyof Endpoints[Path]>(
path: Path,
options: {
method?: Method;
params?: Endpoints[Path][Method] extends { params: infer P } ? P : never;
query?: Endpoints[Path][Method] extends { query: infer Q } ? Q : never;
body?: Endpoints[Path][Method] extends { body: infer B } ? B : never;
}
): Promise<Endpoints[Path][Method] extends { response: infer R } ? R : never> {
// 实现...
return {} as any;
}
// 使用
const users = await api('/users', { method: 'GET', query: { page: 1 } });
// User[]
const user = await api('/users/:id', { method: 'GET', params: { id: '1' } });
// User
注意事项
- 约束顺序:有默认值的类型参数必须在没有默认值的参数之后。例如
<T, U = string>是正确的,<T = string, U>是错误的。 - 约束与默认值的关系:默认类型必须满足约束。例如
<T extends string = number>是错误的,因为 number 不满足 string 约束。 - 泛型推断:当 TypeScript 可以从参数推断类型时,不需要显式指定。但约束不会参与推断,只用于验证推断结果。
- 条件泛型的返回类型:条件泛型的返回类型可能很复杂,建议为使用条件泛型的函数添加详细的 JSDoc 注释。
进阶用法
泛型约束链
// 多层约束链:逐步细化类型
type EntityWithId = { id: string };
type EntityWithTimestamps = EntityWithId & { createdAt: string; updatedAt: string };
type SoftDeletable = EntityWithTimestamps & { deletedAt: string | null };
// 通用仓库接口
interface Repository<T extends EntityWithId> {
findById(id: string): Promise<T | null>;
save(entity: T): Promise<T>;
delete(id: string): Promise<void>;
}
// 带时间戳的仓库
interface TimestampedRepository<T extends EntityWithTimestamps> extends Repository<T> {
findUpdatedSince(date: string): Promise<T[]>;
}
// 软删除仓库
interface SoftDeleteRepository<T extends SoftDeletable> extends TimestampedRepository<T> {
softDelete(id: string): Promise<void>;
findActive(): Promise<T[]>;
}
泛型与类装饰器
// 类型安全的混入函数
type Constructor<T = {}> = new (...args: any[]) => T;
function withValidation<T extends Constructor<{ [key: string]: any }>>(Base: T) {
return class extends Base {
private errors: Map<string, string> = new Map();
validate(): boolean {
this.errors.clear();
for (const [key, value] of Object.entries(this)) {
if (typeof value === 'string' && value.trim() === '') {
this.errors.set(key, `${key} 不能为空`);
}
}
return this.errors.size === 0;
}
getErrors(): ReadonlyMap<string, string> {
return this.errors;
}
};
}