TypeScript 高级类型实战:从泛型到类型体操的完整进阶
TypeScript 的价值不仅是"给 JS 加类型",而是让类型系统成为编译期的逻辑验证器。本文从泛型、条件类型到工具类型的实现原理,系统提升你的类型设计能力,让"类型即文档、类型即测试"落地到日常开发。
一、泛型:类型的参数化
泛型让类型像函数参数一样可复用:
function identity<T>(value: T): T {
return value;
}
const n = identity(42); // number
const s = identity("hello"); // string
泛型约束让参数类型更精确:
interface HasLength {
length: number;
}
function longest<T extends HasLength>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
// longest(42, 1) // 编译错误:number 没有 length
longest("ab", "cde") // 正确
longest([1, 2], [3]) // 正确
二、联合类型与类型收窄
type Result = { ok: true; data: string } | { ok: false; error: string };
function handle(r: Result) {
if (r.ok) {
r.data.toUpperCase(); // 类型收窄为 { ok: true; data: string }
} else {
r.error.toLowerCase(); // 收窄为失败分支
}
}
可辨识联合(Discriminated Union) 是处理复杂状态机的利器:每个分支带一个字面量标签字段,switch 即天然收窄。
三、条件类型与 infer:类型编程的核心
3.1 条件类型
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
3.2 infer:类型推断
// 提取函数返回类型(就是内置 ReturnType 的实现)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = () => Promise<string>;
type R = MyReturnType<Fn>; // Promise<string>
// 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E = ElementOf<string[]>; // string
infer 的价值:让类型系统能从已有类型中"逆向解析"出需要的部分,是类型体操的基本功。
四、映射类型:批量变换类型结构
// 把每个属性变为可空
type Nullable<T> = { [K in keyof T]: T[K] | null };
// 只读版本(内置 Readonly 的实现)
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
interface User {
id: number;
name: string;
}
type NullableUser = Nullable<User>;
// { id: number | null; name: string | null }
五、内置工具类型速查
// 从接口中挑选属性
type PickUser = Pick<User, "id" | "name">;
// 排除属性
type WithoutId = Omit<User, "id">;
// 部分可选 / 全部必填
type PartialUser = Partial<User>;
type RequiredUser = Required<User>;
// 提取/排除联合类型成员
type A = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"
type B = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
// 函数与类工具
type P = Parameters<(a: number, b: string) => void>; // [number, string]
type RT = ReturnType<() => boolean>; // boolean
type CI = ConstructorParameters<new (a: number) => T>; // [number]
六、实际项目中的高级用法
6.1 类型安全的 API 层
type ApiMap = {
"/api/v1/articles": { query: { page: number }; res: Article[] };
"/api/v1/articles/:slug": { params: { slug: string }; res: Article };
};
function get<K extends keyof ApiMap>(
path: K,
opts: ApiMap[K]["query"]
): Promise<ApiMap[K]["res"]>;
请求函数与接口定义绑定,改接口时调用方全部编译报错——这就是类型驱动的重构。
6.2 表单校验的精确类型
type FormState<T> = {
values: T;
errors: { [K in keyof T]?: string };
touched: { [K in keyof T]?: boolean };
};
interface LoginForm {
email: string;
password: string;
}
// errors.email / errors.password 可访问,errors.username 编译报错
6.3 模板字面量类型
type EventName = `on${Capitalize<"click" | "change" | "submit">}`;
// "onClick" | "onChange" | "onSubmit"
模板字面量类型在事件系统、路径拼装、CSS 属性名等场景非常实用。
七、类型体操的三个实战原则
- 先建模后编码:复杂业务先写类型定义,让类型引导实现;
- 工具类型优先:80% 需求用内置工具类型解决,别重复造轮子;
- 体操克制:类型太复杂反而难维护——如果一段类型代码比业务代码还难读,不如退回显式标注。
检查清单:
any出现的地方必须有理由(迁移期除外);- 对外 API 类型全部显式导出;
- 每个泛型都有意义,不为"看起来高级"而用。
结语
TypeScript 的类型系统是"可编程的",掌握泛型、条件类型、infer 与映射类型,你就能把类型从"标注"升级为"约束"。真正的高手写类型不是为了炫技,而是让编译器替你挡住:拼错的字段、传错的参数、漏掉的 null 分支。当你的项目"类型通过即逻辑通过"成为一种常态,TypeScript 才真正兑现了它的承诺。
评论(0)
登录 后参与评论。