TypeScript 类型体操完全指南:从入门到实战
掌握 TypeScript 高级类型技巧,写出更安全的代码
前言
在使用 TypeScript 开发时,你是否遇到过这些场景:
- 想要提取对象的部分属性类型,却不知道如何用工具类型实现
- 面对复杂的泛型约束,只能照搬官方示例,不理解背后的原理
- 看到开源项目中的类型体操代码,感觉像在读天书
类型系统本是 TypeScript 的核心优势,但很多开发者只使用了最基础的类型注解,错过了类型编程带来的巨大收益。
这篇文章将从基础概念出发,带你逐步掌握 TypeScript 类型体操的核心技巧。学完后,你将能够理解条件类型、映射类型、推断等核心概念,手写常用工具类型,并在实际项目中运用类型体操提升代码质量。
一、条件类型:类型世界的三元运算符
1. 基础语法
条件类型是 TypeScript 类型体操的基石,它的语法与 JavaScript 的三元运算符类似:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
这里的 extends 关键字用于类型判断,如果 T 可以赋值给 string,则返回 true,否则返回 false。
2. 分布式条件类型
当条件类型的左侧是泛型参数时,TypeScript 会自动进行分布式判断:
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>; // string[] | number[]
// 等价于 ToArray<string> | ToArray<number>
这个特性在处理联合类型时非常有用,但有时也会带来意外结果。如果需要避免分布式行为,可以用元组包裹泛型:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type A = ToArrayNonDist<string | number>; // (string | number)[]
3. 实战:Extract 和 Exclude
TypeScript 内置的 Extract 和 Exclude 工具类型就是条件类型的经典应用:
// Extract:从联合类型中提取可赋值给 U 的类型
type MyExtract<T, U> = T extends U ? T : never;
type A = MyExtract<'a' | 'b' | 'c', 'a' | 'b'>; // 'a' | 'b'
// Exclude:从联合类型中排除可赋值给 U 的类型
type MyExclude<T, U> = T extends U ? never : T;
type B = MyExclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
二、映射类型:批量处理对象属性
1. 基础语法
映射类型允许你基于现有类型创建新类型,类似于数组的 map 方法:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
interface User {
name: string;
age: number;
}
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; }
2. 属性修饰符
TypeScript 4.1+ 支持在映射类型中添加属性修饰符:
// 可选属性
type Partial<T> = {
[P in keyof T]?: T[P];
};
// 必填属性
type Required<T> = {
[P in keyof T]-?: T[P];
};
// 移除 readonly
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
3. 键的重映射
TypeScript 4.1 引入了 as 语法,允许在映射类型中重命名键:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
三、类型推断:从模式中提取类型
1. infer 关键字
infer 允许你在条件类型中推断出未知类型:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
type A = ReturnType<() => string>; // string
type B = ReturnType<(x: number) => boolean>; // boolean
2. 多重推断
TypeScript 4.7+ 支持在单个条件类型中进行多次推断:
type FirstAndLast<T extends any[]> = T extends [infer First, ...infer _, infer Last]
? [First, Last]
: never;
type A = FirstAndLast<[1, 2, 3, 4]>; // [1, 4]
3. 实战:Promise 解包
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;
type A = UnwrapPromise<Promise<Promise<string>>>; // string
type B = UnwrapPromise<number>; // number
四、常见问题与解决方案
问题 1:条件类型中的 never
当联合类型中的某些分支返回 never 时,这些分支会被自动移除:
type FilterStrings<T> = T extends string ? T : never;
type A = FilterStrings<string | number | boolean>; // string
// number 和 boolean 分支返回 never,被自动过滤
这不是 bug,而是设计行为。利用这个特性可以实现类型过滤。
问题 2:泛型约束过严
// 不推荐的写法
type GetKey<T extends { key: string }> = T['key'];
// 推荐的写法:使用索引访问,更灵活
type GetKey<T, K extends keyof T> = T[K];
问题 3:递归类型导致循环引用
// 不推荐的写法:可能导致类型检查过慢
type DeepReadonly<T> = {
readonly [K in keyof T]: DeepReadonly<T[K]>;
};
// 推荐的写法:添加终止条件
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
五、实战技巧
技巧 1:对象属性路径提取
type KeysOfUnion<T> = T extends T ? keyof T : never;
interface A { a: string; b: number; }
interface B { b: number; c: boolean; }
type K = KeysOfUnion<A | B>; // "a" | "b" | "c"
技巧 2:函数参数类型提取
type FirstParameter<T extends (...args: any[]) => any> =
T extends (arg: infer P, ...args: any[]) => any ? P : never;
type A = FirstParameter<(x: string, y: number) => void>; // string
技巧 3:数组元素类型提取
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type A = ArrayElement<string[]>; // string
type B = ArrayElement<number[]>; // number
技巧 4:深度可选类型
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface User {
name: string;
address: {
city: string;
zip: number;
};
}
type PartialUser = DeepPartial<User>;
// { name?: string; address?: { city?: string; zip?: number; }; }
六、工具与资源推荐
工具 1:TypeScript Playground
- 官网:www.typescriptlang.org/play
- 主要功能:在线编写和调试 TypeScript 代码
- 适用场景:快速验证类型体操代码
工具 2:type-challenges
- GitHub:github.com/type-challe…
- 主要功能:TypeScript 类型体操练习题
- 适用场景:系统性练习类型编程技巧
工具 3:Utility Types
- GitHub:github.com/andnp/Simpl…
- 主要功能:常用工具类型集合
- 适用场景:项目中直接复用
七、最佳实践总结
-
优先使用内置工具类型 -
Partial、Pick、Omit等已覆盖大部分场景 -
复杂类型添加注释 - 类型体操代码可读性差,关键处添加类型注释
-
避免过度抽象 - 类型编程应以提升开发体验为目的,不要为了炫技而写
-
善用类型测试 - 使用
expectType等工具验证类型推导结果 -
关注 TypeScript 版本 - 新版本可能带来更简洁的类型写法
| 场景 | 推荐方案 | 注意事项 |
|---|---|---|
| 对象部分属性 | Pick<T, K> / Omit<T, K> | K 必须是 keyof T |
| 属性可选 | Partial<T> | 递归场景用 DeepPartial |
| 属性必填 | Required<T> | 会移除可选修饰符 |
| 只读对象 | Readonly<T> | 浅层只读 |
| 函数返回值 | ReturnType<T> | 支持 Promise 解包 |
| 实例类型 | InstanceType<T> | T 必须是构造函数 |
八、实战案例:API 响应类型生成
案例背景
假设你有一个后端 API,返回格式统一为:
interface ApiResponse<T> {
code: number;
data: T;
message: string;
}
现在需要编写工具类型,从 API 响应中提取数据类型。
实现步骤
- 定义基础提取类型
- 处理嵌套 Promise 场景
- 支持错误类型提取
完整代码
// 基础 API 响应类型
interface ApiResponse<T> {
code: number;
data: T;
message: string;
}
// 提取响应数据类型
type ExtractResponseData<T> = T extends ApiResponse<infer U> ? U : never;
// 处理 Promise 包装
type UnwrapApi<T> = T extends Promise<infer P>
? ExtractResponseData<P>
: ExtractResponseData<T>;
// 使用示例
type User = { id: number; name: string };
async function getUser(): Promise<ApiResponse<User>> {
// ...
}
type UserType = UnwrapApi<ReturnType<typeof getUser>>; // User
总结
类型体操是 TypeScript 的高级特性,掌握它能让你写出更安全、更灵活的代码。但也要记住,类型系统是为开发服务的工具,不要本末倒置。
学习建议:
- 从内置工具类型开始,理解其实现原理
- 在 type-challenges 上刷题,循序渐进
- 在实际项目中尝试应用,遇到问题再深入学习
- 关注 TypeScript 官方发布,了解新特性
类型编程的世界很大,这篇文章只是入门。真正掌握需要在实践中不断摸索和总结。
参考资料
- TypeScript 官方文档 - 条件类型:www.typescriptlang.org/docs/handbo…
- TypeScript 官方文档 - 映射类型:www.typescriptlang.org/docs/handbo…
- type-challenges:github.com/type-challe…
- Utility Types 集合:github.com/andnp/Simpl…
觉得文章对你有帮助?欢迎点赞收藏,分享给更多需要的朋友!