泛型
让类型也能"传参数",一份代码适配多种类型
🎯 学习目标
学完本章,你将能够:
- 能够定义泛型函数、泛型接口和泛型类实现类型安全复用
- 能够使用 extends 约束泛型参数的范围与形状
- 能够理解 infer 关键字在条件类型中的推断机制
- 能够运用泛型设计可复用的工具类型与工厂函数
- 能够遵循泛型设计原则避免过度抽象与约束过紧
先看一个问题
如何写一个函数,既能处理 string,又能处理 number,还能保留类型信息?
// 方案1:any —— 能用,但丢失了类型信息
function identity(val: any): any {
return val;
}
const result = identity("hello"); // any —— 不知道返回的是 string
// 方案2:写多个函数 —— 太重复
function identityString(val: string): string { return val; }
function identityNumber(val: number): number { return val; }
// 方案3:泛型 —— 完美解决
function identity<T>(val: T): T {
return val;
}
const r1 = identity("hello"); // 自动推断 T = string
const r2 = identity(42); // 自动推断 T = number
泛型让函数、接口、类可以参数化类型,实现类型级别的复用。
泛型是什么
类型的"占位符",使用时再填入具体类型
泛型(Generics)是一种在定义时不指定具体类型,而在使用时才确定类型的机制。就像函数的参数是"值的占位符",泛型的 <T> 就是"类型的占位符"。
打个比方:泛型就像一个模板——定义好格式,用的时候再填入具体内容。
// T 是类型参数,调用时自动推断或手动指定
function identity<T>(val: T): T {
return val;
}
identity("hello"); // T = string,返回 string
identity(42); // T = number,返回 number
identity<string>("hi"); // 显式指定 T = string
Part 1: 泛型函数
最常见的泛型使用场景,掌握它就掌握了泛型的核心思想
写一个函数,既能处理字符串数组,又能处理数字数组,还要保留类型信息——怎么做?泛型函数就是答案。接下来我们从最简单的 identity 函数开始,逐步掌握泛型函数的定义、推断和约束。学完这一节,你就能用泛型改写日常开发中几乎所有的工具函数。
常用泛型函数
从简单到实用的递进示例
// 获取数组第一个元素
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
first([1, 2, 3]); // number | undefined
first(["a", "b"]); // string | undefined
// 安全地读取对象属性
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "张三", age: 25 };
pluck(user, "name"); // string
pluck(user, "age"); // number
// pluck(user, "email"); // 报错!"email" 不是 user 的属性
// 合并两个对象
function merge<A, B>(a: A, b: B): A & B {
return { ...a, ...b };
}
const merged = merge({ name: "张三" }, { age: 25 });
// { name: string; age: number }
Part 2: 泛型接口
API 响应、数据仓库等场景的标准模式
前后端对接时,API 响应的结构是固定的——有 code、message、data。但 data 的类型每个接口都不一样。用泛型接口 ApiResponse<T> 就能一套结构适配所有接口。数据仓库的 CRUD 模板也是同样的思路。来看具体写法。
泛型接口
让接口适配不同的数据类型
// API 响应——最经典的泛型接口
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
interface User { name: string; age: number; }
// 使用时指定 T 的具体类型
const userRes: ApiResponse<User> = {
code: 200,
message: "ok",
data: { name: "张三", age: 25 },
};
const listRes: ApiResponse<User[]> = {
code: 200,
message: "ok",
data: [{ name: "张三", age: 25 }],
};
// 分页响应
interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
}
// 泛型仓库接口——CRUD 的类型安全模板
interface Repository<T> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
create(data: Omit<T, "id">): Promise<T>;
update(id: string, data: Partial<T>): Promise<T>;
delete(id: string): Promise<void>;
}
Part 3: 泛型类
数据结构、事件系统等核心模式的基础
栈、队列、事件发射器——这些数据结构和工具类有个共同特点:它们不关心存储的是什么类型的数据,只关心操作逻辑。泛型类让一个 Stack 既能装数字又能装字符串,而且类型信息全程保留。写一次,到处复用。
泛型类
让类也能参数化类型
// 泛型栈
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
peek(): T | undefined { return this.items[this.items.length - 1]; }
get size(): number { return this.items.length; }
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.pop(); // 2
const stringStack = new Stack<string>();
stringStack.push("hello");
// stringStack.push(42); // 报错!
// 泛型事件发射器
class EventEmitter<T extends string> {
private listeners = new Map<T, Set<Function>>();
on(event: T, fn: Function): () => void {
if (!this.listeners.has(event))
this.listeners.set(event, new Set());
this.listeners.get(event)!.add(fn);
return () => this.listeners.get(event)?.delete(fn);
}
emit(event: T, ...args: unknown[]): void {
this.listeners.get(event)?.forEach(fn => fn(...args));
}
}
type AppEvents = "login" | "logout" | "error";
const emitter = new EventEmitter<AppEvents>();
emitter.on("login", (user: string) => console.log(user));
// emitter.on("unknown", () => {}); // 报错!不是 AppEvents
Part 4: 泛型约束
T 太宽泛?用 extends 给它加限制
泛型的 T 可以是任何类型——但有时候你并不想"来者不拒"。比如写 logLength 函数,T 得有 .length 属性才行。用 extends 给 T 加约束,既保留泛型的灵活性,又确保类型安全。keyof 约束还能让属性名访问绝对安全。
extends 约束
让泛型参数满足特定条件
// 问题:T 太宽泛,无法使用特定属性
function logLength<T>(val: T): void {
// console.log(val.length); // 报错!T 不一定有 length
}
// 解决:用 extends 约束 T 必须有 length 属性
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(val: T): void {
console.log(val.length); // OK!T 保证有 length
}
logLength("hello"); // OK,string 有 length
logLength([1, 2, 3]); // OK,数组有 length
logLength({ length: 10 }); // OK,对象有 length 属性
// logLength(42); // 报错!number 没有 length
keyof 约束
限制参数必须是对象的某个键
// 约束 K 必须是 T 的属性名
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "张三", age: 25 };
getProperty(user, "name"); // string
// getProperty(user, "email"); // 报错!"email" 不是 user 的属性
// 约束为对象类型
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
// new 类型约束——T 必须可以 new
function createInstance<T>(Ctor: new () => T): T {
return new Ctor();
}
class Person { name = "张三"; }
const p = createInstance(Person); // Person
默认泛型参数
泛型参数可以有默认值,像函数参数一样
// 泛型参数可以有默认值
interface ApiResponse<T = unknown, E = string> {
data: T;
error?: E;
}
const res1: ApiResponse = { data: "hello" };
// T = unknown, E = string(都使用默认值)
const res2: ApiResponse<User> = { data: { name: "张三", age: 25 } };
// T = User, E = string
const res3: ApiResponse<User, number> = { data: { name: "张三", age: 25 } };
// T = User, E = number
Part 5: 多泛型参数
一个泛型不够用?那就用两个、三个
map 函数的输入和输出类型不同,需要两个泛型;zip 函数合并两个数组,需要三个。多泛型参数并不复杂——每个泛型各管各的类型流动,TypeScript 会自动推断。这一节看完,你就能理解那些看似复杂的工具函数签名了。
多泛型参数
处理不同类型之间的转换关系
// 两个泛型参数:map 转换
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
map([1, 2, 3], n => n.toString()); // string[]
map(["a", "bb"], s => s.length); // number[]
// 三个泛型参数:zip 合并
function zip<A, B, C>(a: A[], b: B[], fn: (x: A, y: B) => C): C[] {
return a.map((item, i) => fn(item, b[i]));
}
zip([1, 2, 3], ["a", "b", "c"], (n, s) => ({ n, s }));
// [{ n: 1, s: "a" }, { n: 2, s: "b" }, { n: 3, s: "c" }]
综合实战
类型安全的事件系统
// 泛型 + 条件类型:类型安全的 EventEmitter
type EventMap = {
login: { userId: string };
logout: undefined;
error: { message: string; code: number };
};
class TypedEmitter<T extends Record<string, any>> {
on<K extends keyof T>(event: K, fn: (data: T[K]) => void): void {}
emit<K extends keyof T>(event: K, data: T[K]): void {}
}
const typed = new TypedEmitter<EventMap>();
typed.on("login", (data) => {
console.log(data.userId); // 自动推断为 string
});
typed.emit("error", { message: "crash", code: 500 });
// typed.emit("error", { message: "crash" }); // 报错!缺少 code
泛型命名惯例:T(Type)、K(Key)、V(Value)、E(Element)、R(Return)。多个类型参数用有意义的名称:TInput、TOutput。
三种方式对比
any、unknown 和泛型各有所长
| 方式 | 类型安全 | 类型保留 | 适用场景 |
|---|---|---|---|
any | 无 | 丢失 | 快速原型、老代码迁移 |
unknown | 需收窄 | 保留为 unknown | 不确定类型时 |
泛型 <T> | 完整 | 保留具体类型 | 函数/接口/类的类型复用 |
// 对比:同样的函数,三种写法
function idAny(val: any): any { return val; }
function idUnknown(val: unknown): unknown { return val; }
function idGeneric<T>(val: T): T { return val; }
const a = idAny("hi"); // any —— 什么都能做,但不安全
const b = idUnknown("hi"); // unknown —— 必须先收窄才能用
const c = idGeneric("hi"); // string —— 完整保留类型信息
Part 6: 进阶实战
这部分是分水岭。接下来你将看到 infer、条件类型、模板字面量类型、依赖注入容器——它们在生产级 TS 库(Prisma、tRPC、NestJS)里随处可见,但理解成本明显高于前五章。
第一次学泛型,可以跳过本节,直接去看"泛型设计原则"和后面的练习。等你写过几个真实项目、回头再读,会顺畅得多。这不是偷懒,是把难点放到合适的时机。
如果你选择继续——下面这些例子按难度递增。卡住很正常,infer 和递归类型本身就需要反复看几遍。一次读不完没关系。
infer 揭秘
在条件类型中"捕获"子类型
infer 就像类型世界里的解构赋值。你在模式匹配的位置放一个 infer R,TypeScript 会帮你把匹配到的类型提取到 R 里。
infer 只能在 extends ? : 条件类型的右侧出现,并且它不"执行"任何东西——它只是在编译器做模式匹配时,给匹配到的类型起个名字。运行时这段类型信息已经被擦除了,不存在。
// 提取函数返回类型——ReturnType 的实现原理
type MyReturn<T> = T extends (...args: any[]) => infer R ? R : never;
type A = MyReturn<() => string>; // string
type B = MyReturn<(x: number) => boolean>; // boolean
type C = MyReturn<string>; // never(不是函数)
// 提取 Promise 内部类型
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
type P1 = Unwrap<Promise<string>>; // string
type P2 = Unwrap<Promise<Promise<number>>>; // number
type P3 = Unwrap<Promise<Promise<Promise<boolean>>>>; // boolean
// 提取数组第一个元素类型
type Head<T> = T extends [infer F, ...any[]] ? F : never;
type H = Head<[string, number, boolean]>; // string
关于递归:Unwrap<T> 在自己内部又调用了 Unwrap<U>,这就是递归条件类型。TypeScript 会一层层剥开 Promise,直到不再是 Promise 为止。和写递归函数是同一种思路。
类型如何传播
从调用处推断类型参数到函数内部的传播过程
理解泛型的关键是搞清楚"类型是怎么流动的"。来看一个完整的过程:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
// 调用处:传入 string[]
const result = first(["hello", "world"]);
// 1. TS 看到 ["hello", "world"],推断为 string[]
// 2. 参数 arr: T[] 匹配 string[],所以 T = string
// 3. 返回值 T | undefined 变成 string | undefined
// 4. result 的类型:string | undefined

图:泛型类型推断的传播流程
【AI 生图提示词】请生成一张中文技术流程图,风格简洁、配色柔和、适合教学: - 画面主体:从左到右的传播流程,共 4 个节点 - 节点 1:调用处传入参数(例如 first(["hello", "world"])) - 节点 2:TS 推断 T 的具体类型(T = string,用橙色高亮) - 节点 3:T 传播到函数体内所有使用处(T[] → string[],T | undefined → string | undefined) - 节点 4:返回值类型确定(string | undefined) - 底部标注:"类型从调用处流入,经过函数体,再从返回值流出" - 配色建议:浅灰背景,推断过程用橙色箭头,类型标注用蓝色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
核心原则:类型从调用处流入,经过函数体,再从返回值流出。整个过程由 TypeScript 编译器自动完成。
泛型工厂
用泛型创建类型安全的工厂函数
工厂模式 + 泛型 = 类型安全的对象创建。在写库和框架的时候特别有用。
// 类型安全的工厂函数
function createFactory<T>(defaults: T) {
return (overrides: Partial<T> = {}): T => {
return { ...defaults, ...overrides };
};
}
interface User {
name: string;
age: number;
role: "admin" | "user";
}
const createUser = createFactory<User>({
name: "匿名",
age: 0,
role: "user",
});
const u1 = createUser({ name: "张三" });
// { name: "张三", age: 0, role: "user" }
const u2 = createUser({ name: "李四", age: 25, role: "admin" });
// createUser({ role: "super" }); // 报错!"super" 不是合法值
// 泛型 + 约束:带验证的工厂
function createValidated<T extends object>(
schema: { [K in keyof T]: (val: unknown) => T[K] },
) {
return (input: Record<string, unknown>): T => {
const result = {} as T;
for (const key in schema) {
result[key] = schema[key](input[key]);
}
return result;
};
}
泛型函数重载
泛型与函数重载结合,实现类型级别的条件分支
有时候我们希望函数的行为根据输入类型动态变化——传入 string 返回 number,传入 number 返回 string。泛型 + 条件类型 + 函数重载可以优雅地实现这一点。
// 泛型 + 条件类型 = 返回值类型随输入变化
function convert<T extends string | number>(value: T): T extends string ? number : string;
function convert(value: string | number): string | number {
if (typeof value === "string") return Number(value);
return String(value);
}
const a = convert("42"); // number —— TS 知道返回的是数字
const b = convert(42); // string —— TS 知道返回的是字符串
// const c: number = convert(42); // 报错!convert(42) 返回 string
// 实战:类型安全的 JSON 解析器
function parseJSON<T>(input: string, validator: (data: unknown) => data is T): T;
function parseJSON<T>(input: string, validator?: Function): unknown;
function parseJSON<T>(input: string, validator?: Function): T | unknown {
const data = JSON.parse(input);
if (validator && validator(data)) return data as T;
return data;
}
// 有 validator 时返回具体类型,没有时返回 unknown
const safe = parseJSON('{"name":"张三"}', (d: any): d is User => typeof d.name === "string");
// safe: User
注意:重载签名必须和实现签名兼容。实现签名是 string | number,重载签名返回的条件类型必须是这个联合的子集,否则会报错。
深层属性安全
用 keyof + 泛型构建类型安全的多层属性访问
前面我们学过 getProperty,但实际开发中经常需要访问嵌套属性。让 keyof 和泛型结合起来,实现深层类型安全。
// 基础版:单层属性访问
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "张三", age: 25, address: { city: "北京" } };
getProperty(user, "name"); // string
getProperty(user, "age"); // number
// getProperty(user, "phone"); // 报错!"phone" 不是 user 的属性
// 进阶版:类型安全的链式 get
type PathImpl<T, K extends keyof T> =
K extends string
? T[K] extends object
? `${K}.${keyof T[K] & string}` | K
: K
: never;
type Path<T> = PathImpl<T, keyof T>;
// 使用示例
type UserPath = Path<{ name: string; address: { city: string; zip: number } }>;
// "name" | "address" | "address.city" | "address.zip"
设计思路:keyof T 告诉你"对象有哪些属性",T[K] 告诉你"这个属性的类型是什么"。两者组合就能在编译期保证属性访问的绝对安全——拼错属性名直接报错,而不是运行时 undefined。
构造函数签名
用泛型抽象"可以 new 的东西"
在写依赖注入、工厂模式、ORM 这类框架级代码时,你经常需要把类本身当作参数传递。这时就需要构造函数签名。
// 核心类型:描述"可以用 new 调用、返回 T 的构造函数"
type Constructor<T> = new (...args: any[]) => T;
// 用泛型工厂创建实例
function createInstance<T>(Ctor: Constructor<T>, ...args: any[]): T {
return new Ctor(...args);
}
class Person {
constructor(public name: string, public age: number) {}
}
const p = createInstance(Person, "张三", 25);
// p: Person —— 类型完整保留
// 实战:依赖注入容器
class Container {
private registry = new Map<string, Constructor<any>>();
private instances = new Map<string, any>();
register<T>(key: string, Ctor: Constructor<T>): void {
this.registry.set(key, Ctor);
}
resolve<T>(key: string): T {
if (this.instances.has(key)) return this.instances.get(key);
const Ctor = this.registry.get(key);
if (!Ctor) throw new Error(`未注册: ${key}`);
const instance = new Ctor();
this.instances.set(key, instance);
return instance as T;
}
}
const container = new Container();
container.register("logger", ConsoleLogger);
const logger = container.resolve<ConsoleLogger>("logger");
常见陷阱:new (...args: any[]) => T 和 () => T 是不同的。前者是构造函数签名(用 new 调用),后者是普通工厂函数(直接调用)。传 class 时必须用构造函数签名。
泛型设计原则
写出优雅、可维护的泛型代码的四条原则
泛型很强大,但滥用会让代码变成"类型天书"。这里总结四条经过实战验证的设计原则。
原则一:不要过度泛化
// 过度泛化:明明只有两种情况,非要搞泛型
function format<T extends string | number>(val: T): string {
return String(val);
}
// 更好的做法:直接写联合类型,简单明了
function format(val: string | number): string {
return String(val);
}
经验法则:如果泛型只用到一个地方,而且类型数量有限(≤3),联合类型往往更清晰。
原则二:优先让 TS 自动推断
// 不好的写法:到处手动指定
const r1 = identity<string>("hello");
const r2 = first<number>([1, 2, 3]);
// 好的写法:让 TS 自己推断
const r1 = identity("hello"); // 自动推断 T = string
const r2 = first([1, 2, 3]); // 自动推断 T = number
什么时候手动指定:当推断不出来(如无参函数 create<User>())或推断结果不够精确时。
约束设计技巧
原则三和原则四:约束要恰到好处
原则三:约束粒度要合适
// 太松:T 可以是 anything,等于没约束
function getProperty<T>(obj: T, key: string): any {
return (obj as any)[key]; // 完全丢失类型安全
}
// 太严:限制死了只能是特定接口
function getUserName<T extends { name: string; age: number }>(obj: T): string {
return obj.name;
}
// 刚好:只约束需要的属性
function getName<T extends { name: string }>(obj: T): string {
return obj.name;
}
原则四:泛型参数越少越好
// 过多泛型:阅读者需要理解 4 个类型参数
function transform<A, B, C, D>(input: A, ab: (a: A) => B,
bc: (b: B) => C, cd: (c: C) => D): D {
return cd(bc(ab(input)));
}
// 更好的做法:减少显式泛型,让推断工作
function pipe<T>(input: T, ...fns: Function[]): any {
return fns.reduce((acc, fn) => fn(acc), input);
}
总结:能用 1 个泛型参数解决的,不要用 2 个。能推断的,不要手写。约束要恰好覆盖你的需求——不多不少。
泛型与异步
泛型在异步编程中的经典应用模式
前端开发中,异步操作无处不在。泛型让异步代码的类型安全达到和同步代码一样的水平。
// 最常见的模式:泛型 + async/await
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<T>;
}
// 使用时获得完整的类型推断
interface Todo { id: number; title: string; completed: boolean }
const todo = await fetchJson<Todo>("/api/todos/1");
console.log(todo.title); // 类型安全!
// 进阶:带缓存的异步函数
function cachedFetch<T>() {
const cache = new Map<string, Promise<T>>();
return async (url: string): Promise<T> => {
if (cache.has(url)) return cache.get(url)!;
const promise = fetchJson<T>(url);
cache.set(url, promise);
return promise;
};
}
const fetchUser = cachedFetch<User>();
await fetchUser("/api/user/1"); // 网络请求
await fetchUser("/api/user/1"); // 命中缓存
注意:response.json() 返回的是 Promise<any>,用 as Promise<T> 断言不会做运行时校验。在生产代码中,建议搭配运行时验证库(如 zod)做双重保障。
事件系统实战
练习
任务:实现一个类型安全的 EventEmitter。用泛型 + 映射类型定义事件名和事件数据的对应关系,支持 on、emit、off 三个方法。
// 目标效果:
// 1. on("login", (data) => {}) 中 data 自动推断
// 2. emit("login", { userId: "1" }) 参数类型校验
// 3. off("login", handler) 移除监听
// 4. on("unknown", ...) 报错!不在事件映射中
// 你的代码:
参考答案
// 事件映射:事件名 → 数据类型
type EventMap = {
login: { userId: string };
logout: undefined;
error: { message: string; code: number };
};
type Handler<T> = T extends undefined ? () => void : (data: T) => void;
class TypedEmitter<T extends Record<string, any>> {
private listeners = new Map<keyof T, Set<Handler<any>>>();
on<K extends keyof T>(event: K, fn: Handler<T[K]>): () => void {
if (!this.listeners.has(event))
this.listeners.set(event, new Set());
this.listeners.get(event)!.add(fn);
return () => this.off(event, fn);
}
off<K extends keyof T>(event: K, fn: Handler<T[K]>): void {
this.listeners.get(event)?.delete(fn);
}
emit<K extends keyof T>(event: K, ...args: T[K] extends undefined ? [] : [T[K]]): void {
this.listeners.get(event)?.forEach(fn => fn(...args as any));
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on("login", (data) => console.log(data.userId));
emitter.emit("login", { userId: "123" });
// emitter.emit("login", {}); // 报错!缺少 userId
常见问题
问题一:泛型推断不出来
// 泛型推断依赖参数,没有参数就推断不出
function create<T>(): T { return {} as T; }
const obj = create(); // T = unknown,丢失了
解决:显式指定泛型参数 create<User>()。
问题二:约束太严或太松
// 太松:T 可以是任何东西
function print<T>(val: T) { console.log(val); } // OK 但不够精确
// 太严:只接受 string
function print<T extends string>(val: T) { } // number 就进不来
解决:根据实际需要选择约束程度,优先让 TS 自动推断。
试一试
练习一
任务:写一个泛型函数 wrapper,接受任意值并返回包含该值的数组。
// wrapper("hello") 应返回 ["hello"]
// wrapper(42) 应返回 [42]
// 你的代码:
参考答案
function wrapper<T>(val: T): T[] {
return [val];
}
wrapper("hello"); // string[]
wrapper(42); // number[]
练习二
任务:写一个泛型函数 pluck,从对象数组中提取某个属性的所有值。
// pluck([{name:"A",age:1},{name:"B",age:2}], "name")
// 应返回 ["A", "B"]
// 你的代码:
参考答案
function pluck<T, K extends keyof T>(arr: T[], key: K): T[K][] {
return arr.map(item => item[key]);
}
pluck([{name:"A",age:1},{name:"B",age:2}], "name");
// string[]
📝 要点总结
本章核心知识点
泛型基础
类型参数 T、多参数、默认类型参数
约束与推断
extends 约束形状、keyof 约束键、infer 提取类型
泛型函数/接口/类
三种场景的泛型声明方式与最佳实践
设计原则
延迟指定类型、约束最小化、避免嵌套过深
实战模式
泛型工厂、泛型事件系统、泛型异步处理
下一章
泛型让类型也能传参数。下一章我们学习高级类型操作——用类型编程,让类型系统为你工作。