前言
TypeScript 的类型系统是其区别于 JavaScript 的核心竞争力。但很多开发者停留在 interface 和基础的 type 别名上,没有真正发挥类型系统的威力。本文从实战出发,带你深入 TypeScript 的高级类型。
一、泛型约束(Generic Constraints)
泛型本身很灵活,但过于灵活反而失去意义。通过 extends 关键字施加约束,让泛型既灵活又安全。
// ❌ 太宽松:T 可以是任何类型
function getLength<T>(arg: T): number {
// return arg.length; // Error: T 上不存在 length
}
// ✅ 约束 T 必须包含 length 属性
function getLength<T extends { length: number }>(arg: T): number {
return arg.length; // OK
}
getLength("hello"); // 5
getLength([1, 2, 3]); // 3
// getLength(123); // Error: number 没有 length
// 进阶:泛型约束 + keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 30 };
getProperty(user, 'name'); // 'Alice',类型推导为 string
// getProperty(user, 'email'); // Error: 'email' 不存在于 user 类型
二、条件类型(Conditional Types)
条件类型是 TypeScript 中最强大的类型操作符,语法类似三元表达式:T extends U ? X : Y
// 基础条件类型
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<number>; // false
// 实战:提取函数返回类型(简化版 ReturnType)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type FnReturn = MyReturnType<() => Promise<string>>; // Promise<string>
// 递归条件类型 — 展平深层嵌套数组
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type Deep = Flatten<string[][][]>; // string
type Shallow = Flatten<number[]>; // number
三、映射类型(Mapped Types)
映射类型让你基于已有类型创建新类型,是类型体操的核心工具。
// 将所有属性变为可选
type Partial<T> = { [K in keyof T]?: T[K] };
// 将所有属性变为只读
type Readonly<T> = { readonly [K in keyof T]: T[K] };
// 实战:创建一个 API 响应包装类型
type ApiResponse<T> = {
[K in keyof T]: {
value: T[K];
updatedAt: Date;
source: 'cache' | 'server';
};
};
interface User {
name: string;
age: number;
}
type CachedUser = ApiResponse<User>;
// { name: { value: string; updatedAt: Date; source: ... }, age: { ... } }
// 高级技巧:条件映射 — 只保留特定类型属性
type PickByType<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
interface Mixed {
id: number;
name: string;
active: boolean;
count: number;
}
type StringProps = PickByType<Mixed, string>; // { name: string }
type NumberProps = PickByType<Mixed, number>; // { id: number; count: number }
四、模板字面量类型(Template Literal Types)
TypeScript 4.1+ 引入的模板字面量类型,让字符串类型也具备了编程能力。
// CSS 单位约束
type CSSValue = `${number}${'px' | 'rem' | 'em' | '%' | 'vh' | 'vw'}`;
const width: CSSValue = '100px'; // ✅
const height: CSSValue = '50vh'; // ✅
// const size: CSSValue = 'auto'; // ❌ Error
// 事件类型约束
type EventName = 'click' | 'focus' | 'blur' | 'keydown';
type HandlerName = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur' | 'onKeydown'
// 实战:类型安全的路由系统
type Route = '/' | '/about' | `/users/${string}` | `/posts/${number}`;
function navigate(route: Route) { /* ... */ }
navigate('/'); // ✅
navigate('/users/alice'); // ✅
navigate('/posts/42'); // ✅
// navigate('/invalid'); // ❌ Error
// 进阶:递归模板类型 — 路径解析
type SplitPath<S extends string> =
S extends `${infer Head}/${infer Tail}`
? Head | SplitPath<Tail>
: S;
type PathSegments = SplitPath<'api/v1/users/profile'>;
// 'api' | 'v1' | 'users' | 'profile'
五、实战:类型安全的 API 客户端
综合运用以上技术,构建一个完全类型安全的 API 客户端:
// 定义 API 端点和响应类型
interface ApiEndpoints {
'/users': { id: number; name: string }[];
'/users/{id}': { id: number; name: string; email: string };
'/posts': { id: number; title: string }[];
'/posts/{id}': { id: number; title: string; content: string };
}
// 类型安全的请求函数
function request<P extends keyof ApiEndpoints>(
path: P,
params?: Record<string, string>
): Promise<ApiEndpoints[P]> {
const url = params
? path.replace(/\{(\w+)\}/g, (_, k) => params[k])
: path;
return fetch(url).then(r => r.json());
}
// 使用 — 完全类型推导
const users = await request('/users');
// 类型: { id: number; name: string }[]
const user = await request('/users/{id}', { id: '42' });
// 类型: { id: number; name: string; email: string }
// ❌ 编译时就会报错
// const invalid = await request('/invalid');
// const wrong = await request('/users/{id}', { wrong: '42' });
总结
| 技术 | 语法 | 核心用途 |
|---|---|---|
| 泛型约束 | T extends U | 限制类型参数范围,获取智能提示 |
| 条件类型 | T extends U ? X : Y | 根据类型条件分发不同类型 |
| 映射类型 | [K in keyof T] | 遍历属性批量变换类型结构 |
| 模板字面量 | ${prefix}${Suffix} | 字符串级别的类型约束与构造 |
| infer 推断 | infer R | 在条件类型中提取子类型 |
最佳实践:不要为了炫技而写类型体操。好的类型设计应该让 IDE 自动补全更精准、让重构更安全、让代码成为自己的文档。如果类型比业务逻辑还复杂,就该重新审视设计了。