TypeScript 泛型
泛型(Generics)是创建可复用组件的核心特性,允许组件支持多种类型而不是单一类型。
泛型基础
为什么需要泛型
// 不使用泛型 - 类型丢失
function identity(arg: any): any {
return arg;
}
const result = identity("hello"); // result 类型是 any
// 使用泛型 - 保留类型
function identityGeneric<T>(arg: T): T {
return arg;
}
const resultStr = identityGeneric("hello"); // resultStr 类型是 string
const resultNum = identityGeneric(123); // resultNum 类型是 number
解释:泛型 T 是一个类型变量,在调用时由传入的参数类型决定。这样可以保留输入和输出的类型关系。
泛型函数
// 基本语法
function identity<T>(arg: T): T {
return arg;
}
// 调用方式一:类型推断
let output1 = identity("hello"); // string
// 调用方式二:显式指定类型
let output2 = identity<string>("hello"); // string
// 箭头函数写法
const identityArrow = <T>(arg: T): T => arg;
// 在 JSX 中需要使用 extends 避免与 JSX 标签混淆
const identityJSX = <T extends unknown>(arg: T): T => arg;
泛型接口
定义泛型接口
// 泛型接口
interface Container<T> {
value: T;
getValue(): T;
setValue(value: T): void;
}
// 实现
const numberContainer: Container<number> = {
value: 123,
getValue() { return this.value; },
setValue(value) { this.value = value; }
};
const stringContainer: Container<string> = {
value: "hello",
getValue() { return this.value; },
setValue(value) { this.value = value; }
};
函数类型泛型接口
interface GenericFunction<T> {
(arg: T): T;
}
// 使用
const myFunction: GenericFunction<number> = (arg) => arg * 2;
myFunction(5); // 10
泛型类
基本泛型类
class Box<T> {
private content: T;
constructor(content: T) {
this.content = content;
}
getContent(): T {
return this.content;
}
setContent(content: T): void {
this.content = content;
}
}
// 使用
const numberBox = new Box<number>(123);
console.log(numberBox.getContent()); // 123
const stringBox = new Box<string>("hello");
console.log(stringBox.getContent()); // "hello"
泛型约束类
interface Comparable {
compareTo(other: this): number;
}
class SortedList<T extends Comparable> {
private items: T[] = [];
add(item: T): void {
// 按顺序插入
let i = 0;
while (i < this.items.length && this.items[i].compareTo(item) < 0) {
i++;
}
this.items.splice(i, 0, item);
}
getItems(): T[] {
return this.items;
}
}
泛型约束
基本约束
// 约束 T 必须有 length 属性
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength("hello"); // 5
logLength([1, 2, 3, 4, 5]); // 5
logLength({ length: 10 }); // 10
// logLength(123); // 错误:number 没有 length 属性
解释:使用 extends 关键字约束泛型类型必须满足某些条件。
在泛型约束中使用类型参数
// 获取对象的某个属性
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = {
name: "张三",
age: 25,
city: "北京"
};
const name = getProperty(user, "name"); // string
const age = getProperty(user, "age"); // number
// getProperty(user, "gender"); // 错误:不存在 "gender" 属性
使用类类型约束
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
}
// 约束 T 必须是 Animal 或其子类
function createInstance<T extends Animal>(ctor: new (...args: any[]) => T, name: string): T {
return new ctor(name);
}
const dog = createInstance(Dog, "旺财");
console.log(dog.name); // "旺财"
多类型参数
多个泛型参数
// 多个类型参数
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
const p1 = pair("name", "张三"); // [string, string]
const p2 = pair(1, "first"); // [number, string]
const p3 = pair(true, 100); // [boolean, number]
// 对象映射
function mapObject<K extends string, V, R>(
obj: Record<K, V>,
mapper: (value: V, key: K) => R
): Record<K, R> {
const result = {} as Record<K, R>;
for (const key in obj) {
result[key] = mapper(obj[key], key);
}
return result;
}
const numbers = { a: 1, b: 2, c: 3 };
const doubled = mapObject(numbers, (v) => v * 2);
// { a: 2, b: 4, c: 6 }
泛型默认类型
// 默认类型
interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
}
// 使用默认类型
const response1: ApiResponse = {
code: 200,
message: "success",
data: { name: "张三" }
};
// 指定类型
const response2: ApiResponse<string> = {
code: 200,
message: "success",
data: "hello"
};
const 类型参数
TypeScript 5.0 引入了 const 类型参数修饰符,它可以让泛型函数在推断类型时获得更精确的字面量类型,而无需在调用时手动添加 as const。
问题背景
考虑这样一个函数,我们希望它返回传入数组的确切类型:
function getNamesExactly<T extends readonly string[]>(names: T): T {
return names;
}
// 默认情况下,TypeScript 推断为更宽泛的类型
const names = getNamesExactly(["Alice", "Bob", "Charlie"]);
// names 的类型是 string[],而不是 ["Alice", "Bob", "Charlie"]
问题分析:TypeScript 默认会将数组推断为更通用的 string[] 类型,而不是具体的字面量元组类型。这在某些场景下会丢失有用的类型信息。
传统的解决方案
以前,开发者需要在调用时使用 as const 来获得精确的类型:
const names = getNamesExactly(["Alice", "Bob", "Charlie"] as const);
// names 的类型是 readonly ["Alice", "Bob", "Charlie"]
缺点:每个调用者都需要记住添加 as const,容易遗漏且代码冗余。
使用 const 类型参数
TypeScript 5.0 允许在类型参数上添加 const 修饰符,使函数自动进行 as const 风格的推断:
function getNamesExactly<const T extends readonly string[]>(names: T): T {
return names;
}
// 自动推断为字面量元组类型
const names = getNamesExactly(["Alice", "Bob", "Charlie"]);
// names 的类型是 readonly ["Alice", "Bob", "Charlie"]
// 可以通过显式指定类型来覆盖
const names2 = getNamesExactly<string[]>(["Alice", "Bob", "Charlie"]);
// names2 的类型是 string[]
const 类型参数的工作原理
const 修饰符告诉 TypeScript 在推断类型参数时,优先选择更具体的字面量类型:
// 没有 const 修饰符
function createArray<T extends readonly unknown[]>(...items: T): T {
return items;
}
const arr1 = createArray(1, 2, 3);
// arr1 的类型是 [number, number, number]
// 有 const 修饰符
function createArrayConst<const T extends readonly unknown[]>(...items: T): T {
return items;
}
const arr2 = createArrayConst(1, 2, 3);
// arr2 的类型是 readonly [1, 2, 3]
实际应用示例
示例一:路由定义
function defineRoutes<const T extends Record<string, string>>(
routes: T
): T {
return routes;
}
const routes = defineRoutes({
home: "/",
about: "/about",
contact: "/contact"
});
// routes.home 的类型是 "/",而不是 string
// 这样可以获得更精确的类型检查
type HomeRoute = typeof routes.home; // "/"
示例二:事件类型定义
function createEventEmitter<const T extends string[]>(
...events: T
): { on<E extends T[number]>(event: E, handler: () => void): void } {
const handlers = new Map<string, Set<() => void>>();
return {
on(event, handler) {
if (!handlers.has(event)) {
handlers.set(event, new Set());
}
handlers.get(event)!.add(handler);
}
};
}
const emitter = createEventEmitter("click", "focus", "blur");
// 事件名称被精确推断为 "click" | "focus" | "blur"
emitter.on("click", () => {}); // OK
emitter.on("focus", () => {}); // OK
// emitter.on("hover", () => {}); // 错误:hover 不是有效的事件类型
示例三:配置对象构建
function createConfig<const T extends {
api: { baseUrl: string; timeout: number };
features: Record<string, boolean>;
}>(config: T): T {
return config;
}
const config = createConfig({
api: {
baseUrl: "https://api.example.com",
timeout: 5000
},
features: {
darkMode: true,
notifications: false
}
});
// config.features.darkMode 的类型是 true(字面量类型)
const 与约束的交互
需要注意的是,const 修饰符不会拒绝可变值,也不会要求不可变约束。当推断的类型与约束冲突时,会回退到约束类型:
// 约束是可变数组
function getFirst<const T extends string[]>(arr: T): T[number] | undefined {
return arr[0];
}
const a = getFirst(["a", "b", "c"]);
// a 的类型是 "a" | "b" | "c" | undefined
// 如果推断的 readonly 类型与可变约束冲突
function mutableArray<T extends string[]>(arr: T): T {
return arr;
}
// 正常工作,推断回退到 string[]
const b = mutableArray(["a", "b", "c"]);
// b 的类型是 string[]
最佳实践:当使用 const 类型参数时,约束应该使用 readonly 修饰符:
function betterGetFirst<const T extends readonly string[]>(
arr: T
): T[number] | undefined {
return arr[0];
}
const c = betterGetFirst(["a", "b", "c"]);
// c 的类型是 "a" | "b" | "c" | undefined
const 的限制
const 修饰符只影响直接传入的字面量表达式:
function foo<const T extends string>(arg: T): T {
return arg;
}
// 字面量会被精确推断
const a = foo("hello"); // a 的类型是 "hello"
// 变量不会被影响
const str = "hello";
const b = foo(str); // b 的类型是 string,不是 "hello"
// 已经有类型的表达式也不会被影响
const c = foo("hello" as string); // c 的类型是 string
const 与泛型约束的组合
// 组合使用 const 和复杂约束
function createMatcher<
const T extends readonly (string | number | boolean)[]
>(...patterns: T): (value: T[number]) => boolean {
return (value) => patterns.includes(value as any);
}
const isStatus = createMatcher("pending", "approved", "rejected");
// isStatus 的类型是 (value: "pending" | "approved" | "rejected") => boolean
isStatus("pending"); // OK
isStatus("approved"); // OK
// isStatus("unknown"); // 错误
最佳实践总结
-
在需要精确字面量类型的泛型函数中使用
const修饰符:可以避免调用者手动添加as const -
约束使用
readonly:与const修饰符配合使用,避免类型回退
// 推荐写法
function example<const T extends readonly unknown[]>(items: T): T {
return items;
}
-
明确使用场景:
const类型参数最适合用于定义配置、路由、事件类型等需要精确字面量类型的场景 -
可以显式覆盖:如果需要更宽泛的类型,调用者可以显式指定类型参数
泛型工具类型
Partial
将类型 T 的所有属性变为可选:
interface User {
name: string;
age: number;
email: string;
}
function updateUser(user: User, updates: Partial<User>): User {
return { ...user, ...updates };
}
const user: User = { name: "张三", age: 25, email: "[email protected]" };
const updated = updateUser(user, { age: 26 });
// { name: "张三", age: 26, email: "[email protected]" }
Required
将类型 T 的所有属性变为必需:
interface Props {
name?: string;
age?: number;
}
const props: Required<Props> = {
name: "张三",
age: 25
// 都必须提供
};
Readonly
将类型 T 的所有属性变为只读:
interface Point {
x: number;
y: number;
}
const point: Readonly<Point> = { x: 10, y: 20 };
// point.x = 15; // 错误:只读属性
Pick
从类型 T 中选取部分属性:
interface User {
id: number;
name: string;
email: string;
password: string;
}
// 只选取 id 和 name
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string; }
Omit
从类型 T 中排除部分属性:
interface User {
id: number;
name: string;
email: string;
password: string;
}
// 排除 password
type PublicUser = Omit<User, "password">;
// { id: number; name: string; email: string; }
Record
创建键类型为 K,值类型为 T 的对象类型:
type Role = "admin" | "user" | "guest";
const permissions: Record<Role, string[]> = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"]
};
ReturnType
获取函数类型的返回类型:
function getUser() {
return { name: "张三", age: 25 };
}
type User = ReturnType<typeof getUser>;
// { name: string; age: number; }
Parameters
获取函数类型的参数类型:
function greet(name: string, age: number): string {
return `Hello, ${name}!`;
}
type GreetParams = Parameters<typeof greet>;
// [string, number]
Extract 和 Exclude
从联合类型中提取或排除类型:
type T = string | number | boolean;
// Extract:提取
type StringOrNumber = Extract<T, string | number>;
// string | number
// Exclude:排除
type NotString = Exclude<T, string>;
// number | boolean
NonNullable
从类型中排除 null 和 undefined:
type T = string | null | undefined;
type SafeT = NonNullable<T>;
// string
Awaited
获取 Promise 的解析类型:
type PromiseType = Promise<Promise<string>>;
type ResolvedType = Awaited<PromiseType>;
// string
条件类型
条件类型允许根据类型关系选择类型:
// 基本语法
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
分布式条件类型
当条件类型作用于联合类型时,会自动分发:
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[](不是 (string | number)[])
// 使用 [T] 禁用分发
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNoDistribute<string | number>;
// (string | number)[]
infer 关键字
infer 用于在条件类型中推断类型:
// 推断数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type A = ElementOf<string[]>; // string
type B = ElementOf<number[]>; // number
// 推断函数返回类型
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
type C = ReturnOf<() => string>; // string
type D = ReturnOf<(x: number) => boolean>; // boolean
// 推断 Promise 值类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type E = UnwrapPromise<Promise<string>>; // string
type F = UnwrapPromise<number>; // number
// 推断元组第一个元素
type First<T> = T extends [infer F, ...any[]] ? F : never;
type G = First<[1, 2, 3]>; // 1
type H = First<[]>; // never
复杂示例:深度提取 Promise 类型
type DeepAwaited<T> = T extends Promise<infer U>
? DeepAwaited<U>
: T;
type A = DeepAwaited<Promise<Promise<Promise<string>>>>;
// string
映射类型
映射类型基于旧类型创建新类型:
// 将所有属性变为可选
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// 将所有属性变为只读
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// 添加前缀
type Prefixed<T> = {
[K in keyof T as `prefix_${string & K}`]: T[K];
};
type User = { name: string; age: number };
type PrefixedUser = Prefixed<User>;
// { prefix_name: string; prefix_age: number }
Key Remapping(键重映射)
// 过滤特定类型的属性
type StringPropsOnly<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface User {
name: string;
age: number;
email: string;
}
type StringUserProps = StringPropsOnly<User>;
// { name: string; email: string }
模板字面量类型
// 构造事件名称
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type FocusEvent = EventName<"focus">; // "onFocus"
// 解析模板字面量
type ParseEvent<T> = T extends `on${infer E}` ? Uncapitalize<E> : never;
type ParsedClick = ParseEvent<"onClick">; // "click"
递归条件类型
// 深度只读
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K];
};
interface Config {
database: {
host: string;
port: number;
};
cache: {
ttl: number;
};
}
type ReadonlyConfig = DeepReadonly<Config>;
// 所有嵌套属性都是 readonly
泛型最佳实践
命名约定
泛型类型参数的命名应该有意义,遵循以下约定:
// ✅ 推荐:使用有意义的名称
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// ✅ 常见约定:单个大写字母
// T - Type(类型)
// K - Key(键)
// V - Value(值)
// E - Element(元素)
// R - Result/Return(返回值)
// P - Parameters(参数)
// ✅ 对于复杂场景,使用描述性名称
interface ApiResponse<TData, TError = Error> {
data: TData | null;
error: TError | null;
success: boolean;
}
// ✅ 约定俗成的名称
type Container<TItem> = {
items: TItem[];
add(item: TItem): void;
remove(predicate: (item: TItem) => boolean): void;
};
解释:虽然单字母泛型参数很常见,但在复杂类型中,使用描述性名称可以提高代码可读性。当泛型参数有特定含义时(如表示数据、错误、配置等),优先使用描述性名称。
尽可能使用类型推断
让 TypeScript 自动推断泛型类型,而不是手动指定:
// ❌ 不推荐:不必要的显式类型指定
const result = identity<string>("hello");
// ✅ 推荐:让 TypeScript 推断
const result = identity("hello"); // 自动推断为 string
// ✅ 需要显式指定的场景:类型推断不准确
function combine<T>(arr1: T[], arr2: T[]): T[] {
return [...arr1, ...arr2];
}
const combined = combine([1, 2], ["a", "b"]); // 错误!推断 T 为 number | string
const combined2 = combine<number | string>([1, 2], ["a", "b"]); // 正确
解释:类型推断可以减少代码冗余,提高可读性。但当推断结果不准确或需要更宽松的类型时,应该显式指定类型参数。
约束泛型以提高类型安全
使用约束限制泛型参数的范围,避免过于宽泛的类型:
// ❌ 问题:类型过于宽泛
function getLength<T>(arg: T): number {
// 无法访问 length,因为 T 可能是任何类型
return 0;
}
// ✅ 正确:添加约束
interface HasLength {
length: number;
}
function getLength<T extends HasLength>(arg: T): number {
return arg.length; // 安全:已知 T 有 length 属性
}
getLength("hello"); // OK:string 有 length
getLength([1, 2, 3]); // OK:array 有 length
getLength({ length: 10 }); // OK:对象有 length
// getLength(123); // 错误:number 没有 length
使用泛型工厂函数
当需要创建类型安全的工厂函数时,使用泛型确保返回类型的正确性:
// 泛型工厂函数
function createFactory<T>(constructor: new (...args: any[]) => T) {
return function(...args: any[]): T {
return new constructor(...args);
};
}
class User {
constructor(public name: string, public age: number) {}
}
const createUser = createFactory(User);
const user = createUser("张三", 25); // 类型是 User,不是 any
优先使用内置工具类型
TypeScript 提供了丰富的内置工具类型,优先使用它们而不是自己实现:
// ✅ 使用内置工具类型
type PartialUser = Partial<User>;
type ReadonlyUser = Readonly<User>;
type UserKeys = keyof User;
type UserName = User["name"];
// ❌ 避免:重复造轮子
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
常见陷阱与注意事项
陷阱一:泛型类型的类型擦除
泛型在编译后会被擦除,运行时无法获取泛型类型信息:
function isType<T>(value: unknown): value is T {
// ❌ 错误:无法在运行时检查泛型类型
// return value instanceof T; // 编译错误
return true;
}
// ✅ 正确:传入类型守卫函数或构造函数
function isInstanceOf<T>(value: unknown, ctor: new (...args: any[]) => T): value is T {
return value instanceof ctor;
}
const result = isInstanceOf(new Date(), Date); // true
解释:TypeScript 的泛型是编译时特性,编译为 JavaScript 后所有泛型类型信息都会被移除。如果需要在运行时检查类型,必须传入类型守卫或构造函数。
陷阱二:泛型与静态成员
泛型类的静态成员不能使用类的泛型类型参数:
class Container<T> {
private items: T[] = [];
// ✅ 实例成员可以使用 T
add(item: T): void {
this.items.push(item);
}
// ❌ 错误:静态成员不能使用 T
// static defaultValue: T; // 编译错误
// ✅ 静态方法可以有自己的泛型参数
static create<U>(item: U): Container<U> {
const container = new Container<U>();
container.add(item);
return container;
}
}
解释:静态成员属于类本身,而不是类的实例。由于泛型类型参数是在实例化时指定的,静态成员无法访问实例的类型参数。
陷阱三:泛型约束的陷阱
泛型约束可能会导致意外的类型收窄:
interface A {
foo(): void;
}
interface B {
bar(): void;
}
// 问题:约束可能导致意外的类型推断
function process<T extends A | B>(value: T): T {
if ("foo" in value) {
// value 的类型是 T & A,而不是 A
value.foo();
} else {
// value 的类型是 T & B,而不是 B
value.bar();
}
return value;
}
陷阱四:泛型与函数重载
泛型函数与重载结合时要小心:
// 问题:重载签名与实现签名不一致
function makeArray<T>(x: T): T[];
function makeArray<T>(x: T, y: T): T[];
function makeArray<T>(...args: T[]): T[] {
return args;
}
// 这看起来没问题,但是...
const arr1 = makeArray(1, 2, 3); // number[]
const arr2 = makeArray(1, "2", true); // (string | number | boolean)[] - 可能不是预期的
// ✅ 改进:使用更严格的类型
function makeStrictArray<T>(x: T): [T];
function makeStrictArray<T>(x: T, y: T): [T, T];
function makeStrictArray<T>(...args: T[]): T[] {
return args;
}
const arr3 = makeStrictArray(1, 2); // [number, number] - 精确类型
陷阱五:泛型默认值的顺序
有默认值的类型参数必须在必需的类型参数之后:
// ❌ 错误:有默认值的参数在必需参数之前
// interface ApiResponse<TData = any, TError> { ... }
// ✅ 正确:必需参数在前,有默认值的参数在后
interface ApiResponse<TError, TData = any> {
data: TData | null;
error: TError | null;
}
const response1: ApiResponse<string> = { // TError 是 string,TData 默认是 any
data: { name: "张三" },
error: null
};
const response2: ApiResponse<string, { name: string }> = {
data: { name: "张三" },
error: null
};
陷阱六:泛型类型推断的边界情况
TypeScript 的类型推断有时会产生意外结果:
// 问题:类型推断为联合类型的数组
function wrapInArray<T>(...items: T[]): T[] {
return items;
}
const mixed = wrapInArray(1, "a", true);
// T 被推断为 string | number | boolean
// 结果类型是 (string | number | boolean)[]
// 解决方案:使用元组保留精确类型
function wrapInTuple<T extends any[]>(...items: T): T {
return items;
}
const tuple = wrapInTuple(1, "a", true);
// 结果类型是 [number, string, boolean]
陷阱七:分布式条件类型
条件类型作用于联合类型时会自动分发:
type ToArray<T> = T extends any ? T[] : never;
// 分布式:对联合类型的每个成员分别应用
type Result = ToArray<string | number>;
// string[] | number[],而不是 (string | number)[]
// 禁用分发:使用元组包裹
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNoDistribute<string | number>;
// (string | number)[]
解释:分布式条件类型在某些场景下很有用(如 Exclude、Extract),但当你想要处理整个联合类型时,需要使用元组包裹来禁用分发。
泛型实战示例
通用 API 客户端
interface ApiResponse<T> {
code: number;
data: T;
message: string;
}
class ApiClient {
async get<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
return response.json();
}
async post<T, D>(url: string, data: D): Promise<ApiResponse<T>> {
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(data)
});
return response.json();
}
}
// 使用
const client = new ApiClient();
const users = await client.get<User[]>("/api/users");
const newUser = await client.post<User, CreateUserDto>("/api/users", {
name: "张三",
email: "[email protected]"
});
类型安全的事件发射器
type EventHandler<T = any> = (data: T) => void;
class EventEmitter<EventMap extends Record<string, any>> {
private handlers: Partial<{ [K in keyof EventMap]: EventHandler<EventMap[K]>[] }> = {};
on<K extends keyof EventMap>(event: K, handler: EventHandler<EventMap[K]>): void {
if (!this.handlers[event]) {
this.handlers[event] = [];
}
this.handlers[event]!.push(handler);
}
emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
const handlers = this.handlers[event];
if (handlers) {
handlers.forEach(handler => handler(data));
}
}
off<K extends keyof EventMap>(event: K, handler: EventHandler<EventMap[K]>): void {
const handlers = this.handlers[event];
if (handlers) {
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
}
}
// 使用
interface MyEvents {
userCreated: { id: number; name: string };
userDeleted: { id: number };
}
const emitter = new EventEmitter<MyEvents>();
emitter.on("userCreated", (data) => {
console.log(`User created: ${data.name}`);
});
emitter.emit("userCreated", { id: 1, name: "张三" });
泛型设计模式
Builder 模式
使用泛型实现类型安全的 Builder 模式:
class ObjectBuilder<T> {
private obj: Partial<T> = {};
with<K extends keyof T>(key: K, value: T[K]): this {
this.obj[key] = value;
return this;
}
build(): T {
// 运行时检查确保所有必需属性都已设置
return this.obj as T;
}
}
interface User {
id: number;
name: string;
email: string;
age?: number;
}
const user = new ObjectBuilder<User>()
.with("id", 1)
.with("name", "张三")
.with("email", "[email protected]")
.with("age", 25)
.build();
解释:Builder 模式在构建复杂对象时非常有用。通过泛型,我们可以确保 with 方法的 key 必须是对象的属性名,value 必须是对应的属性类型,实现完全的类型安全。
策略模式
使用泛型实现类型安全的策略模式:
interface Strategy<TInput, TOutput> {
execute(input: TInput): TOutput;
}
class Context<TInput, TOutput> {
constructor(private strategy: Strategy<TInput, TOutput>) {}
setStrategy(strategy: Strategy<TInput, TOutput>): void {
this.strategy = strategy;
}
execute(input: TInput): TOutput {
return this.strategy.execute(input);
}
}
// 具体策略
class StringReverseStrategy implements Strategy<string, string> {
execute(input: string): string {
return input.split("").reverse().join("");
}
}
class StringLengthStrategy implements Strategy<string, number> {
execute(input: string): number {
return input.length;
}
}
// 使用
const context = new Context(new StringReverseStrategy());
const reversed = context.execute("hello"); // "olleh"
context.setStrategy(new StringLengthStrategy());
const length = context.execute("hello"); // 5
解释:策略模式允许在运行时切换算法。通过泛型,我们可以确保策略的输入输出类型与上下文类型匹配。
仓储模式
使用泛型实现通用的数据访问层:
interface Repository<T> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
save(entity: T): Promise<T>;
delete(id: string): Promise<boolean>;
}
interface BaseEntity {
id: string;
createdAt: Date;
updatedAt: Date;
}
class InMemoryRepository<T extends BaseEntity> implements Repository<T> {
private items: Map<string, T> = new Map();
async findById(id: string): Promise<T | null> {
return this.items.get(id) ?? null;
}
async findAll(): Promise<T[]> {
return Array.from(this.items.values());
}
async save(entity: T): Promise<T> {
this.items.set(entity.id, entity);
return entity;
}
async delete(id: string): Promise<boolean> {
return this.items.delete(id);
}
}
// 使用
interface Product extends BaseEntity {
name: string;
price: number;
}
const productRepo = new InMemoryRepository<Product>();
await productRepo.save({
id: "p1",
name: "商品A",
price: 99.9,
createdAt: new Date(),
updatedAt: new Date()
});
const product = await productRepo.findById("p1"); // Product | null
解释:仓储模式是领域驱动设计中的常见模式。通过泛型,我们可以创建一个通用的数据访问接口,适用于任何实体类型,同时保持类型安全。
装饰器模式
使用泛型实现类型安全的装饰器:
interface Component<T> {
operation(input: T): T;
}
class ConcreteComponent<T> implements Component<T> {
operation(input: T): T {
return input;
}
}
class LoggingDecorator<T> implements Component<T> {
constructor(private wrapped: Component<T>) {}
operation(input: T): T {
console.log(`Input: ${JSON.stringify(input)}`);
const result = this.wrapped.operation(input);
console.log(`Output: ${JSON.stringify(result)}`);
return result;
}
}
class CachingDecorator<T> implements Component<T> {
private cache = new Map<string, T>();
constructor(private wrapped: Component<T>) {}
operation(input: T): T {
const key = JSON.stringify(input);
if (this.cache.has(key)) {
console.log("Cache hit!");
return this.cache.get(key)!;
}
const result = this.wrapped.operation(input);
this.cache.set(key, result);
return result;
}
}
// 使用
let component: Component<string[]> = new ConcreteComponent<string[]>();
component = new LoggingDecorator(component);
component = new CachingDecorator(component);
component.operation(["a", "b", "c"]);
工厂模式
使用泛型实现类型安全的工厂:
interface Product {
type: string;
use(): void;
}
interface ProductConstructor<T extends Product> {
new (...args: any[]): T;
type: string;
}
class ProductFactory {
private static registry = new Map<string, ProductConstructor<any>>();
static register<T extends Product>(ctor: ProductConstructor<T>): void {
this.registry.set(ctor.type, ctor);
}
static create<T extends Product>(type: string, ...args: any[]): T {
const ctor = this.registry.get(type);
if (!ctor) {
throw new Error(`Unknown product type: ${type}`);
}
return new ctor(...args) as T;
}
}
// 具体产品
class ConcreteProductA implements Product {
type = "A";
constructor(private name: string) {}
use() {
console.log(`Using product A: ${this.name}`);
}
}
(ConcreteProductA as any).type = "A";
class ConcreteProductB implements Product {
type = "B";
constructor(private value: number) {}
use() {
console.log(`Using product B with value: ${this.value}`);
}
}
(ConcreteProductB as any).type = "B";
// 注册
ProductFactory.register(ConcreteProductA);
ProductFactory.register(ConcreteProductB);
// 创建
const productA = ProductFactory.create<ConcreteProductA>("A", "测试A");
const productB = ProductFactory.create<ConcreteProductB>("B", 42);
解释:工厂模式通过将对象的创建逻辑封装起来,使客户端代码无需知道具体类的构造细节。泛型确保工厂返回的产品类型与期望类型一致。
泛型与其他特性的配合
泛型与装饰器
function logged<T extends { new (...args: any[]): {} }>(
constructor: T,
context: ClassDecoratorContext
) {
return class extends constructor {
constructor(...args: any[]) {
console.log(`Creating instance of ${constructor.name}`);
super(...args);
console.log(`Instance created`);
}
};
}
@logged
class UserService {
constructor(private name: string) {}
}
const service = new UserService("main");
// 输出:
// Creating instance of UserService
// Instance created
泛型与异步
// 异步泛型工具
async function asyncMap<T, R>(
items: T[],
mapper: (item: T, index: number) => Promise<R>
): Promise<R[]> {
const results: R[] = [];
for (let i = 0; i < items.length; i++) {
results.push(await mapper(items[i], i));
}
return results;
}
// 使用
const urls = ["/api/1", "/api/2", "/api/3"];
interface User {
id: number;
name: string;
}
const users = await asyncMap(urls, async (url) => {
const response = await fetch(url);
return response.json() as Promise<User>;
});
// users 的类型是 User[]
泛型与类型守卫
// 类型守卫工厂
function createTypeGuard<T>(check: (value: unknown) => value is T) {
return check;
}
const isString = createTypeGuard((value: unknown): value is string => {
return typeof value === "string";
});
const isNumberArray = createTypeGuard((value: unknown): value is number[] => {
return Array.isArray(value) && value.every(v => typeof v === "number");
});
function process(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase()); // value 是 string
} else if (isNumberArray(value)) {
console.log(value.reduce((a, b) => a + b, 0)); // value 是 number[]
}
}
变型注解(Variance Annotations)
变型(Variance)是类型理论中的概念,描述泛型类型参数之间的替换关系。理解变型对于编写类型安全的泛型代码至关重要。
什么是变型?
变型描述了当类型 A 是类型 B 的子类型时,Container<A> 和 Container<B> 之间的关系。
考虑一个简单的例子:Cat 是 Animal 的子类型。那么 Producer<Cat> 和 Producer<Animal> 之间是什么关系?
协变(Covariance)
如果 Cat 是 Animal 的子类型,那么 Producer<Cat> 也是 Producer<Animal> 的子类型,这种关系称为协变。
interface Producer<T> {
make(): T; // 生产 T 类型的值
}
class Animal {
name: string = "动物";
}
class Cat extends Animal {
meow() { console.log("喵~"); }
}
// 协变示例
function useProducer(producer: Producer<Animal>) {
const animal = producer.make();
console.log(animal.name);
}
const catProducer: Producer<Cat> = {
make() { return new Cat(); }
};
// ✅ 正确:Producer<Cat> 可以赋值给 Producer<Animal>
// 因为 Cat 是 Animal 的子类型,Producer 也是协变的
useProducer(catProducer);
解释:协变意味着类型关系保持方向一致。因为 Cat 是 Animal,所以能生产 Cat 的生产者也能生产 Animal(毕竟 Cat 就是 Animal)。
逆变(Contravariance)
如果 Cat 是 Animal 的子类型,那么 Consumer<Animal> 是 Consumer<Cat> 的子类型(方向相反),这种关系称为逆变。
interface Consumer<T> {
consume(item: T): void; // 消费 T 类型的值
}
// 逆变示例
function useConsumer(consumer: Consumer<Cat>) {
consumer.consume(new Cat());
}
const animalConsumer: Consumer<Animal> = {
consume(animal) { console.log(`消费: ${animal.name}`); }
};
// ✅ 正确:Consumer<Animal> 可以赋值给 Consumer<Cat>
// 这看起来反直觉,但逻辑是正确的:
// 能消费任何 Animal 的消费者,当然也能消费 Cat(因为 Cat 就是 Animal)
useConsumer(animalConsumer);
解释:逆变意味着类型关系反转。一个能处理任何 Animal 的消费者,当然能处理 Cat,因为 Cat 是 Animal 的子集。
不变(Invariance)
如果两种类型之间没有替换关系,称为不变。
interface Container<T> {
get(): T; // 生产 T
set(value: T): void; // 消费 T
}
// Container 既生产又消费 T,所以是不变的
const catContainer: Container<Cat> = {
get() { return new Cat(); },
set(value) { console.log(value.meow()); }
};
// ❌ 错误:Container<Cat> 不能赋值给 Container<Animal>
// 因为如果可以,调用 set(new Animal()) 会传入一个非 Cat 的 Animal
// 而 Container<Cat>.set 期望的是 Cat,调用 meow() 会出错
const animalContainer: Container<Animal> = catContainer; // 错误!
TypeScript 中的变型推断
TypeScript 使用结构类型系统,会自动推断泛型类型的变型:
interface Producer<T> {
make(): T;
}
interface Consumer<T> {
consume(item: T): void;
}
interface Container<T> {
get(): T;
set(value: T): void;
}
// TypeScript 自动推断:
// Producer<T> 对 T 是协变的
// Consumer<T> 对 T 是逆变的
// Container<T> 对 T 是不变的
变型注解语法
在极少数情况下,TypeScript 的变型推断可能不准确(通常涉及循环类型)。此时可以使用变型注解来显式指定:
// out 关键字表示协变
interface Producer<out T> {
make(): T;
}
// in 关键字表示逆变
interface Consumer<in T> {
consume(item: T): void;
}
// in out 组合表示不变
interface Container<in out T> {
get(): T;
set(value: T): void;
}
何时使用变型注解?
变型注解几乎从不需要。 TypeScript 能正确推断绝大多数类型的变型。只有在极少数涉及特定循环类型的情况下,才可能需要手动指定变型注解。即使在这种情况,也应该先确认是否真的需要。
官方文档多次强调:
Don't try to use variance annotations to change typechecking behavior; this is not what they are for
不要尝试用变型注解改变类型检查行为;这不是它们的用途
变型注解的真正作用:
变型注解只在实例化比较时有效,在结构比较时完全不起作用。这意味着你不能用变型注解来"强制"改变类型的结构行为:
// ❌ 错误的尝试:试图用变型注解改变结构行为
interface BadExample<out T> {
get(): T;
set(value: T): void; // 同时生产和消费 T,结构上是不变的
}
// 这个注解不会生效!TypeScript 会检测到结构上的不变性
// 变型注解只能帮助推断,不能改变结构
可能需要变型注解的场景(极其罕见):
- 某些循环类型导致推断错误:
// 极少数情况下,复杂的循环引用可能导致推断问题
// 即使在这种情况,也要先确认是否真的需要注解
interface Node<out T> {
value: T;
children: Node<T>[]; // 自引用可能导致推断问题
}
- 类型检查性能优化:只有在运行性能分析器后,确认变型推断是性能瓶颈时才考虑。
调试用途:
变型注解有时可用于调试,因为 TypeScript 会检查注解是否与结构变型一致:
// TypeScript 会检查变型注解是否正确
interface Producer<out T> { // ✅ 正确:结构上是协变的
make(): T;
}
interface Consumer<in T> { // ✅ 正确:结构上是逆变的
consume(item: T): void;
}
interface Container<in out T> { // ✅ 正确:结构上是不变的
get(): T;
set(value: T): void;
}
调试完成后,应该移除这些注解。
变型注解的限制
变型注解只影响实例化时的类型比较,不影响结构比较:
interface Producer<out T> {
make(): T;
}
// ✅ 实例化比较:使用变型注解
const catProducer: Producer<Cat> = { make: () => new Cat() };
const animalProducer: Producer<Animal> = catProducer; // OK
// ❌ 结构比较:变型注解无效
const literal = {
make(): number { return 42; }
};
const stringProducer: Producer<string | number> = literal; // 结构比较
// 变型注解在这里不影响行为
实际应用示例
类型安全的观察者模式
// 使用变型注解确保类型安全
interface Observer<in T> {
notify(data: T): void;
}
interface Subject<out T> {
subscribe(observer: Observer<T>): void;
}
class NewsAgency implements Subject<string> {
private observers: Observer<string>[] = [];
subscribe(observer: Observer<string>): void {
this.observers.push(observer);
}
publish(news: string): void {
this.observers.forEach(o => o.notify(news));
}
}
class NewsReader implements Observer<string> {
notify(news: string): void {
console.log(`收到新闻: ${news}`);
}
}
const agency = new NewsAgency();
const reader = new NewsReader();
agency.subscribe(reader);
agency.publish("重大新闻!");
泛型事件系统
// 事件处理器是逆变的
type EventHandler<in T> = (event: T) => void;
// 事件发射器是协变的
interface EventEmitter<out T> {
on(handler: EventHandler<T>): void;
emit(event: T): void;
}
// 实际实现
class SimpleEventEmitter<T> implements EventEmitter<T> {
private handlers: EventHandler<T>[] = [];
on(handler: EventHandler<T>): void {
this.handlers.push(handler);
}
emit(event: T): void {
this.handlers.forEach(h => h(event));
}
}
// 使用
interface ClickEvent {
type: "click";
x: number;
y: number;
}
const clickEmitter = new SimpleEventEmitter<ClickEvent>();
clickEmitter.on((event) => {
console.log(`点击位置: (${event.x}, ${event.y})`);
});
clickEmitter.emit({ type: "click", x: 100, y: 200 });
变型最佳实践
- 优先依赖自动推断:TypeScript 能正确处理绝大多数情况
- 理解结构类型:变型是结构类型系统的自然属性
- 不要滥用注解:变型注解无法改变类型的结构行为
- 用于调试时:变型注解可以帮助发现变型推断错误
// ✅ 正确使用:用于调试和确认变型
interface MyProducer<out T> {
produce(): T;
}
// 如果注解与结构不符,TypeScript 会报错
interface MyContainer<in out T> {
get(): T;
set(value: T): void;
}
小结
本章我们学习了 TypeScript 泛型:
- 泛型基础:理解泛型的本质和作用,掌握泛型函数的语法
- 泛型接口和类:创建可复用的泛型组件
- 泛型约束:限制泛型参数的范围,提高类型安全
- 多类型参数:使用多个泛型参数处理复杂场景
- 泛型默认类型:提供默认类型简化使用
- const 类型参数:TypeScript 5.0 新特性,获得精确的字面量类型
- 内置工具类型:Partial、Pick、Omit 等实用工具
- 条件类型与 infer:实现高级类型推导
- 映射类型:基于现有类型创建新类型
- 最佳实践与陷阱:避免常见错误,写出高质量代码
- 设计模式:使用泛型实现类型安全的设计模式
- 变型注解:理解协变、逆变和不变的概念,TypeScript 会自动推断变型,几乎从不需要手动指定
泛型是 TypeScript 最强大的特性之一,掌握泛型能让你写出更灵活、更安全、更可复用的代码。记住,TypeScript 的类型系统是结构化的,变型是自然涌现的属性,绝大多数情况下无需手动指定变型注解。
练习
基础练习
- 实现一个泛型函数
first,返回数组的第一个元素 - 创建一个泛型
Stack类,包含 push、pop、peek、isEmpty 方法 - 实现一个类型安全的
Object.keys包装函数 - 使用泛型实现一个简单的依赖注入容器
进阶练习
- 实现一个
DeepPartial<T>类型,递归地将所有属性变为可选 - 实现一个
DeepRequired<T>类型,递归地将所有属性变为必需 - 创建一个泛型函数
pick,从对象中选取指定的属性 - 实现一个类型安全的
EventEmitter类,支持事件名称和参数类型的约束
挑战练习
- 实现一个类型安全的 SQL 查询构建器(使用泛型确保表名、字段名的正确性)
- 创建一个泛型的状态机实现,确保状态转换的类型安全
- 实现一个类型安全的 JSON Schema 验证器