TypeScript 高级类型体操:从入门到实战的完整指南
背景
TypeScript 已成为前端开发的标配工具,但很多开发者只停留在基础类型定义层面,对高级类型特性如泛型约束、条件类型、映射类型等知之甚少。掌握这些高级技巧,不仅能提升代码的类型安全性,还能减少大量重复的运行时校验代码。
本文将带你从基础概念出发,通过实战案例逐步掌握 TypeScript 高级类型编程技巧。
核心概念
1. 泛型约束(Generic Constraints)
泛型是 TypeScript 类型系统的基石,但单纯使用泛型往往不够,我们需要对泛型参数进行约束。
// 基础泛型
function identity<T>(arg: T): T {
return arg;
}
// 泛型约束:限制 T 必须有 length 属性
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength("hello"); // ✅ 字符串有 length
logLength([1, 2, 3]); // ✅ 数组有 length
logLength(123); // ❌ 数字没有 length,编译报错
2. 条件类型(Conditional Types)
条件类型允许根据类型关系进行逻辑判断,是构建复杂类型的利器。
// 基础条件类型
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// 实战案例:提取函数返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R = ReturnType<() => string>; // string
type R2 = ReturnType<(x: number) => boolean>; // boolean
3. 映射类型(Mapped Types)
映射类型可以批量转换类型的属性,是构建工具类型的核心。
// 将所有属性变为可选
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
// 将所有属性变为只读
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
// 实战:深度只读
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
interface User {
name: string;
profile: {
age: number;
address: {
city: string;
};
};
}
type ReadonlyUser = DeepReadonly<User>;
// profile 和 address 都变成了只读
实战案例
案例 1:类型安全的 API 请求封装
// 定义 API 响应类型
interface ApiResponse<T> {
code: number;
data: T;
message: string;
}
// 定义请求参数类型
type QueryParams = Record<string, string | number | boolean>;
// 类型安全的请求函数
async function request<T>(
url: string,
options?: {
method?: "GET" | "POST" | "PUT" | "DELETE";
body?: object;
query?: QueryParams;
}
): Promise<ApiResponse<T>> {
const response = await fetch(url, {
method: options?.method || "GET",
headers: { "Content-Type": "application/json" },
body: options?.body ? JSON.stringify(options.body) : undefined,
});
return response.json();
}
// 使用示例
interface UserInfo {
id: string;
name: string;
email: string;
}
// 自动推断返回类型
const result = await request<UserInfo>("/api/user/123");
console.log(result.data.name); // 类型安全!
案例 2:自动生成表单类型
// 从数据类型生成表单类型
type FormType<T> = {
[P in keyof T]: T[P] extends string
? string | undefined
: T[P] extends number
? number | undefined
: T[P];
};
interface UserData {
id: number;
name: string;
email: string;
age: number;
}
// 表单类型:所有字段都可选
type UserForm = FormType<Partial<UserData>>;
// 验证函数类型
type Validator<T> = (value: T[keyof T]) => boolean | string;
function createForm<T>(
initialValues: FormType<T>,
validators?: Partial<Record<keyof T, Validator<T>>>
) {
// 表单逻辑实现
return {
values: initialValues,
validate: () => true,
};
}
案例 3:事件系统类型推导
// 定义事件映射
interface EventMap {
click: { x: number; y: number };
scroll: { scrollTop: number };
input: { value: string };
}
// 类型安全的事件发射器
class TypedEventEmitter<T extends Record<string, object>> {
private listeners = new Map<keyof T, Set<Function>>();
on<K extends keyof T>(event: K, listener: (data: T[K]) => void) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(listener);
}
emit<K extends keyof T>(event: K, data: T[K]) {
const set = this.listeners.get(event);
set?.forEach((fn) => fn(data));
}
}
// 使用
const emitter = new TypedEventEmitter<EventMap>();
emitter.on("click", (data) => {
console.log(data.x, data.y); // 类型自动推导!
});
emitter.emit("click", { x: 100, y: 200 }); // ✅
emitter.emit("click", { x: "wrong" }); // ❌ 类型错误
最佳实践
1. 善用工具类型组合
// 组合多个工具类型
type OptionalExceptId<T extends { id: unknown }> = Omit<T, "id"> & {
id: T["id"];
} & Partial<Omit<T, "id">>;
interface Item {
id: string;
name: string;
price: number;
}
// 更新时:id 必填,其他可选
type UpdateItem = OptionalExceptId<Item>;
2. 使用 infer 提取类型
// 提取 Promise 值类型
type PromiseValue<T> = T extends Promise<infer V> ? V : T;
type Result = PromiseValue<Promise<string>>; // string
// 提取数组元素类型
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type Elem = ArrayElement<string[]>; // string
3. 类型守卫配合类型谓词
interface Fish { swim: () => void }
interface Bird { fly: () => void }
function isFish(pet: Fish | Bird): pet is Fish {
return "swim" in pet;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // 类型收窄为 Fish
} else {
pet.fly(); // 类型收窄为 Bird
}
}
常见问题
Q1: any 和 unknown 有什么区别?
let a: any = "hello";
a.foo(); // 编译通过,运行时报错 ❌
let u: unknown = "hello";
u.foo(); // 编译报错 ✅
if (typeof u === "string") {
u.toUpperCase(); // 安全使用
}
结论:优先使用 unknown,避免使用 any。
Q2: 什么时候用 type,什么时候用 interface?
- interface:定义对象形状、需要扩展/合并时
- type:联合类型、交叉类型、工具类型时
// interface 可扩展
interface Animal { name: string }
interface Animal { age: number } // 合并
// type 更灵活
type ID = string | number;
type Point = { x: number } & { y: number };
Q3: 如何处理复杂的动态键值?
// 使用 Record 和模板字面量类型
type EventName = `on${Capitalize<string>}`;
type Handlers = Record<EventName, Function>;
// 更精确的动态键
type Actions = "create" | "update" | "delete";
type ActionHandlers = {
[K in Actions as `${K}Handler`]: () => void;
};
// 结果: { createHandler: () => void; updateHandler: () => void; ... }
总结
TypeScript 高级类型虽然学习曲线较陡,但掌握后能显著提升代码质量:
- 类型安全:在编译时捕获更多错误
- 智能提示:IDE 自动补全更准确
- 代码文档:类型即文档,自解释性强
- 重构友好:修改类型即知影响范围
建议从日常开发中的痛点出发,逐步引入高级类型技巧。不要过度设计,够用就好。记住:类型系统是工具,不是目的。
推荐资源:
- TypeScript 官方文档
- TypeScript Deep Dive(在线免费书籍)
- GitHub 上的 type-challenges 练习题
0 评论
评论区
登录 后参与评论