TypeScript 类型体操指南
TypeScript 的类型系统是图灵完备的,这意味着你可以在类型层面完成复杂的逻辑运算。"类型体操"虽然听起来像炫技,但掌握它能让你写出更安全、更智能的类型定义。
核心价值:编译时捕获错误、智能提示增强、减少运行时检查、API 类型安全
基础工具类型
TypeScript 内置了一些实用工具类型:
| 工具类型 | 作用 | 示例 |
|---|---|---|
| Partial<T> | 所有属性可选 | Partial<User> |
| Required<T> | 所有属性必选 | Required<User> |
| Readonly<T> | 所有属性只读 | Readonly<Config> |
| Pick<T, K> | 选取部分属性 | Pick<User, 'id' | 'name'> |
| Omit<T, K> | 排除部分属性 | Omit<User, 'password'> |
| Record<K, V> | 构造对象类型 | Record<string, number> |
条件类型
基础语法
// 条件类型基本形式
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// 嵌套条件类型
type TypeName<T> =
T extends string ? 'string' :
T extends number ? 'number' :
T extends boolean ? 'boolean' :
T extends undefined ? 'undefined' :
T extends Function ? 'function' :
'object';
分布式条件类型
当条件类型作用于联合类型时,会自动分配到每个成员:
// 分布式条件类型
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[],不是 (string | number)[]
// 避免分布式行为
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// (string | number)[]
注意:用
[T] 包裹可以阻止分布式行为,这在某些场景下很有用。
infer 关键字
infer 用于在条件类型中推断类型:
// 提取函数返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: 'Alice' };
}
type UserReturn = ReturnType<typeof getUser>;
// { id: number; name: string; }
// 提取函数参数类型
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type Params = Parameters<(a: string, b: number) => void>;
// [a: string, b: number]
// 提取 Promise 值类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Result = UnwrapPromise<Promise<string>>; // string
// 提取数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : T;
type Item = ElementType<string[]>; // string
映射类型
基础映射
// 将所有属性变为可选
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// 将所有属性变为只读
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// 添加前缀
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};
interface User {
id: number;
name: string;
}
type ApiUser = Prefixed<User, 'api'>;
// { apiId: number; apiName: string; }
高级映射技巧
// 过滤属性
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed {
id: number;
name: string;
email: string;
age: number;
}
type StringProps = OnlyStrings<Mixed>;
// { name: string; email: string; }
// 根据 key 过滤
type ExcludeId<T> = {
[K in keyof T as Exclude<K, 'id'>]: T[K];
};
// 递归映射(深度 readonly)
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
interface Config {
server: {
host: string;
port: number;
};
db: {
name: string;
tables: string[];
};
}
type ReadonlyConfig = DeepReadonly<Config>;
// 所有层级都是 readonly
模板字面量类型
基础用法
// 字符串拼接
type Greeting<T extends string> = `Hello, ${T}!`;
type Message = Greeting<'World'>; // "Hello, World!"
// 联合类型展开
type Color = 'red' | 'blue' | 'green';
type Size = 'small' | 'medium' | 'large';
type Button = `${Size}-${Color}`;
// "small-red" | "small-blue" | "small-green" |
// "medium-red" | "medium-blue" | "medium-green" |
// "large-red" | "large-blue" | "large-green"
内置字符串工具
type Str = 'hello world';
type Upper = Uppercase<Str>; // "HELLO WORLD"
type Lower = Lowercase<Str>; // "hello world"
type Cap = Capitalize<Str>; // "Hello world"
type Uncap = Uncapitalize<Str>; // "hello world"
// 实际应用:事件处理器类型
type EventName = 'click' | 'focus' | 'blur';
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
高级模式匹配
// 提取字符串前缀
type RemovePrefix<T extends string, P extends string> =
T extends `${P}${infer Rest}` ? Rest : T;
type Result = RemovePrefix<'getUserById', 'get'>; // "UserById"
// 提取字符串后缀
type RemoveSuffix<T extends string, S extends string> =
T extends `${infer Rest}${S}` ? Rest : T;
type Result2 = RemoveSuffix<'UserController', 'Controller'>; // "User"
// 驼峰转连字符
type CamelToKebab<S extends string> =
S extends `${infer First}${infer Rest}`
? First extends Uppercase<First>
? `-${Lowercase<First>}${CamelToKebab<Rest>}`
: `${First}${CamelToKebab<Rest>}`
: S;
type Kebab = CamelToKebab<'backgroundColor'>; // "background-color"
实战案例
1. 路由参数推断
// 从路由路径提取参数类型
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractParams<'/users/:id/posts/:postId'>;
// "id" | "postId"
// 构建参数对象类型
type RouteParams<Path extends string> = {
[K in ExtractParams<Path>]: string;
};
function get<Path extends string>(
path: Path,
params: RouteParams<Path>
) {
// 实现...
}
get('/users/:id/posts/:postId', { id: '1', postId: '2' }); // ✅ 类型安全
2. API 响应类型推导
// 根据 API 路径自动推导响应类型
interface ApiEndpoints {
'/users': User[];
'/users/:id': User;
'/posts': Post[];
'/posts/:id': Post;
}
type Endpoint = keyof ApiEndpoints;
type ApiResponse<Path extends Endpoint> = ApiEndpoints[Path];
async function fetchApi<Path extends Endpoint>(
path: Path
): Promise<ApiResponse<Path>> {
const response = await fetch(path);
return response.json();
}
const users = await fetchApi('/users'); // User[]
const user = await fetchApi('/users/:id'); // User
3. 深度 Pick
// 支持嵌套路径的 Pick
type DeepPick<T, Path extends string> =
Path extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? { [K in Key]: DeepPick<T[K], Rest> }
: never
: Path extends keyof T
? Pick<T, Path>
: never;
interface User {
id: number;
profile: {
name: string;
email: string;
address: {
city: string;
country: string;
};
};
}
type UserName = DeepPick<User, 'profile.name'>;
// { profile: { name: string } }
type UserAddress = DeepPick<User, 'profile.address.city'>;
// { profile: { address: { city: string } } }
性能与最佳实践
性能警告:复杂的递归类型可能显著增加编译时间。TypeScript 有递归深度限制(约 1000 层)。
- 避免过度复杂:类型体操应服务于实际需求,而非炫技
- 添加类型注释:复杂类型添加注释说明用途
- 拆分类型:将复杂类型拆分为可复用的小类型
- 善用工具类型:优先使用内置工具类型
- 测试类型:使用
expectType等工具验证类型正确性
总结
TypeScript 类型体操的核心技能:
- ✅ 条件类型:
T extends U ? X : Y - ✅ 类型推断:
infer关键字 - ✅ 映射类型:
[K in keyof T] - ✅ 模板字面量:
`${T}` - ✅ 递归类型:注意性能和深度限制