开篇
当我们说 TypeScript 带来"类型安全",并不只是把变量标个 number 或 string。真正让 TS 强大的,是它对类型层面的抽象能力:泛型、条件类型、映射类型、模板字面量类型、infer 等等。这些"高级类型"让我们能在编译期表达复杂的业务约束、提升可维护性与自文档化程度,同时在大型前端/全栈项目中显著降低回归风险。本文将系统梳理 TypeScript 高级类型的核心能力、常用技巧与实践案例,帮你从"能用"走向"好用",甚至"用得漂亮"。
基础回顾
-
泛型与约束
- 泛型让类型可参数化:function wrap(value: T): { value: T } { return { value } }
- 通过 extends 约束:type HasId<T extends { id: string }> = T
-
联合与交叉
- 联合:type A = "on" | "off"
- 交叉:type B = { id: string } & { name: string } // 组合两者属性
-
keyof 与索引访问
- 获取键集合:type Keys = keyof { id: number; name: string } // "id" | "name"
- 索引访问:type ValueOf = T[keyof T]
-
类型别名 vs 接口
- 接口可扩展(声明合并),类型别名更灵活(可用于联合、映射、条件类型)。在"表达复杂组合"时通常更偏向类型别名。
映射类型与键重映射
- 基本映射:对属性做批量变换
ts
type ReadonlyDeep<T> = {
readonly [K in keyof T]: T[K] extends object ? ReadonlyDeep<T[K]> : T[K];
};
- 键重映射:使用 as 重命名键
ts
type PrefixKeys<T, P extends string> = {
[K in keyof T as `${P}${Extract<K, string>}`]: T[K];
};
type Original = { id: number; name: string };
type Prefixed = PrefixKeys<Original, "user_">;
// => { user_id: number; user_name: string }
- 模板字面量类型:与字符串操作结合,轻松生成 API 路由/事件名等
ts
type RouteParam<Name extends string> = `/api/${Name}`;
type UserRoute = RouteParam<"user">; // "/api/user"
条件类型与 infer
- 条件类型:按约束分支
ts
type Awaited<T> = T extends Promise<infer U> ? U : T;
// Awaited<Promise<number>> => number
- 分布式条件类型:联合类型会"自动拆分"逐个套用
- 阻断分布:用方括号包裹避免分布行为
ts
type ToString<T> = T extends number ? `${T}` : never;
type R1 = ToString<1 | 2>; // "1" | "2"(分布)
type NonDist<T> = [T] extends [number] ? `${T}` : never;
type R2 = NonDist<1 | 2>; // never(阻断分布后整体判断失败)
- infer 的常用提取:函数参数、返回值、构造参数、Promise 内部、数组元素类型
ts
type ElementType<T> = T extends (infer U)[] ? U : never;
type Args<F> = F extends (...args: infer P) => any ? P : never;
type Ret<F> = F extends (...args: any[]) => infer R ? R : never;
内置工具类型精读
- 结构类:Partial、Required、Readonly、Pick、Record、Omit
- 集合类:Exclude、Extract、NonNullable
- 函数类:ReturnType、Parameters、ConstructorParameters、InstanceType
- 上下文辅助:ThisType(常配合对象字面量的 this)
- 实用示例:参数与返回值提取
ts
function makePair(a: number, b: string) { return { a, b }; }
type P = Parameters<typeof makePair>; // [number, string]
type R = ReturnType<typeof makePair>; // { a: number; b: string }
satisfies 与 const 断言
- satisfies:保持"值的宽泛性"同时校验其满足某个类型形状,避免过度缩窄
ts
const routes = {
home: "/",
user: (id: string) => `/user/${id}`,
} satisfies Record<string, string | ((...args: any[]) => string)>;
// routes 类型仍保留具体结构;若不满足形状,编译错误
- const 断言:把对象/数组/字符串缩窄为字面量,利于精准类型推断
ts
const config = {
mode: "dark",
shortcuts: ["save", "open"],
} as const;
// "dark" 而非 string;["save", "open"] 而非 string[]
可赋值性与变体(函数参数/返回值)
- 函数参数常为逆变或双变,可能导致看似可赋值但不安全的情况
- 经验法则:尽量让函数类型更"严格/具体"的参数、更"宽泛"的返回值,降低误用
品牌化类型(Nominal Typing)
- 解决"string 都能赋值"的问题,引入轻量"品牌"区分同形不同义
ts
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
function toUserId(x: string): UserId {
return x as UserId;
}
标签联合与状态机建模
- 标签联合(Discriminated Union)让分支穷尽检查成立
ts
type Shape =
| { kind: "circle"; r: number }
| { kind: "rect"; w: number; h: number };
function area(s: Shape) {
switch (s.kind) {
case "circle": return Math.PI * s.r * s.r;
case "rect": return s.w * s.h;
default: const _exhaustive: never = s; return _exhaustive;
}
}
- 用于前端状态流转、服务响应类型、WebSocket 事件等,保障分支完整性
变长元组与高级组合
- 变长元组让你在类型层面"操作参数列表"
ts
type Push<T extends any[], V> = [...T, V];
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type X = Push<[number, string], boolean>; // [number, string, boolean]
type H = Head<[number, string]>; // number
常见陷阱与注意事项
- 过度类型体操:复杂类型会拖慢编译与提示,尽量封装为工具类型或稳定接口
- 分布式条件类型的意外行为:当联合入参不该分布时,用 [T] 包裹阻断
- any 与 unknown
- any 跳过检查;unknown 需先收窄后使用,更安全
- 编译器选项
- 优先开启严格模式;在需要时考虑 noUncheckedIndexedAccess、exactOptionalPropertyTypes,提升细节安全
- 推断边界
- 某些上下文推断有限时,用泛型参数或显式类型补强
- 与运行时的差距
- 类型只在编译期存在,运行时校验可结合 Zod/valibot 等以保证真正的数据安全
应用场景
- SDK 与 API 客户端:自动从接口定义生成请求/响应类型,保障集成正确性
- 表单系统:字段映射、校验规则与错误消息的类型安全建模
- 事件系统与日志:通过模板字面量与标签联合确保事件名与载荷一致
- 设计系统/组件库:Props、主题 Token、样式配置的强类型约束
实战:类型安全的 API 客户端小示例
ts
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint<Req, Res> = {
path: string;
method: HttpMethod;
// 约束请求与响应类型
request: Req;
response: Res;
};
type GetUserReq = { id: string };
type GetUserRes = { id: string; name: string; email?: string };
const getUser = {
path: "/api/user",
method: "GET",
request: {} as GetUserReq,
response: {} as GetUserRes,
} satisfies Endpoint<GetUserReq, GetUserRes>;
type Client = {
call<E extends Endpoint<any, any>>(ep: E, req: E["request"]): Promise<E["response"]>;
};
declare const client: Client;
// 使用时完全类型安全
async function demo() {
const res = await client.call(getUser, { id: "u_123" });
// res: GetUserRes,email 可选,使用前需判空
}
- 进一步可用键重映射把后端字段转换为前端命名规范
- 结合模板字面量为路径拼接生成类型安全的路由字符串
实践准则
- 优先表达业务约束:让类型成为"文档 + 静态检查"
- 工具类型复用:把常见体操封装成局部工具类型
- 控制复杂度:为可读性命名中间类型;避免内联过长条件类型
- 与运行时合流:类型保证形状,运行时保证数据可信