1. 技术难点:类型系统为什么要"编程"
TypeScript 的类型系统并不是"给变量标个类型"这么简单------它本身是一门图灵完备的、可编程的类型语言。当你需要处理:
- 从接口/联合类型派生新类型:比如"排除某个字段"、"取出某属性类型"、"把数组元素类型拆出来"。
- 让类型随输入变化:传一个字符串返回字符串、传数组返回元素类型------这种"输入不同、输出不同"的类型逻辑。
- 条件分支:根据类型是否满足某个约束,决定最终类型。
新手只会用 string/number/interface 是够的,但到大型组件库、框架类型定义、工具类型(Partial、Pick、Omit)时,就必须懂类型编程 (俗称"类型体操")。核心三件套:泛型 + 条件类型 + 映射类型。
2. 完整解法:三个核心武器
2.1 泛型(Generic):让类型"可变"
泛型 = 类型的"参数"。定义时用 <T> 占位,使用时指明具体类型:
ts
// 泛型函数:输入什么类型,返回什么类型
function identity<T>(value: T): T {
return value;
}
const a = identity('hi'); // a: string(自动推断)
const b = identity(123); // b: number
// 泛型接口 / 泛型类
interface ApiResponse<T> {
code: number;
data: T;
}
const resp: ApiResponse<{ id: number }> = {
code: 0,
data: { id: 1 },
};
泛型的价值:一次定义、多种类型复用,且类型安全和返回值关联(不再是 any)。
2.2 条件类型:类型世界的 if/else
用 T extends X ? A : B 表达"如果 T 满足 X 约束,类型是 A,否则是 B":
ts
// 基本条件类型
type IsString<T> = T extends string ? true : false;
type A = IsString<'hi'>; // type A = true
type B = IsString<number>; // type B = false
// 结合 never:过滤类型
type Exclude<T, U> = T extends U ? never : T;
type C = Exclude<'a' | 'b' | 'c', 'a'>; // type C = 'b' | 'c'
never 的妙用 :条件类型中返回 never 相当于"把这个类型从联合里过滤掉"。所以 Exclude、Extract、NonNullable 都是基于条件类型+never 实现的。
2.3 分布式条件类型(Distributive Conditional Types)
当 T 是联合类型 时,条件类型会自动对联合的每一项分别求值------这是类型体操的隐藏杀手:
ts
// 如果直接 T extends ...,联合类型会逐项分发
type ToArray<T> = T extends unknown ? T[] : never;
type R = ToArray<'a' | 'b'>; // type R = 'a'[] | 'b'[](分发,不是 ('a'|'b')[])
// 想整体处理不分发?用 [] 包裹:
type ToArrayNoDistribute<T> = [T] extends [unknown] ? T[] : never;
type S = ToArrayNoDistribute<'a' | 'b'>; // type S = ('a'|'b')[]
踩坑点 :写条件类型时,如果不想要分发行为,一定要用 [T] 包裹,否则联合多元素时行为和你预期的不一样。
2.4 映射类型(Mapped Types):批量改造
用 keyof + 遍历键生成新类型:
ts
// 遍历对象的所有键
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Partial<T> = { [K in keyof T]?: T[K] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
// keyof + 条件类型:从一个类型提取符合某条件的键
type KeysOfType<T, U> = {
[K in keyof T]: T[K] extends U ? K : never
}[keyof T];
interface User {
name: string;
age: number;
isAdmin: boolean;
}
type StringKeys = KeysOfType<User, string>; // type StringKeys = 'name'
[keyof T] 的用法:把"值"(联合)取出来,结合条件类型过滤键。
3. 实战:手写常用的工具类型(理解 TS 内置类型原理)
TS 内置的 Partial/Pick/Omit 等,都可以自己实现,从而理解它们的原理:
ts
// Exclude:排除联合中的某些类型
type MyExclude<T, U> = T extends U ? never : T;
// Omit:从对象剔除某些键
type MyOmit<T, K extends keyof T> = {
[P in MyExclude<keyof T, K>]: T[P];
};
// 条件 + 泛型:确保参数是数组并取元素类型
type Flatten<T> = T extends Array<infer E> ? E : T;
type El = Flatten<number[]>; // type El = number(infer 提取数组元素)
// infer:在条件类型中"提取"子类型(高级但极有用)
type ReturnType<T> = T extends (...args: any) => infer R ? R : any;
type Fn = (x: number) => string;
type RT = ReturnType<Fn>; // type RT = string
infer :允许你在条件类型里声明一个"待推断的类型变量",用于从结构里提取(数组元素、函数返回值、Promise 结果等)------这是 ReturnType、PromiseType 等高级工具的核心。
4. 要点总结
- 泛型 :类型的参数化,
<T>占位复用,让返回值和输入关联。 - 条件类型 :
T extends X ? A : B是类型的 if/else;返回never即过滤。 - 分布式条件类型 :联合类型会自动逐项分发,不想分发用
[T]包裹(隐藏坑)。 - 映射类型 :
keyof+ 遍历键批量改造对象类型(Readonly/Partial/Pick 的原理)。 - infer:条件类型里提取子类型,是 ReturnType/PromiseType 等高级工具的基石。
- 实战价值:理解后能自己写工具类型、看懂框架(Vue/React/zustand)的类型定义、用强类型约束业务。
一句话:类型体操 = 泛型(参数)+ 条件类型(分支)+ 映射类型(批改)+ infer(提取)。这四个工具组合起来,TypeScript 的类型系统就成了你程序里的"第二套可验证的逻辑"。