主题
第 10 章 · TypeScript 高级类型
一句话开篇:基础类型让你"写出能跑的代码",高级类型让你"写出能复用的库"——本章带你从写类型走到计算类型。
0. 生活类比:从"快餐盒"到"智能流水线"
回想上一章我们已经会用 interface、type、union,相当于会用碗、盘子、调味瓶。但要开一家连锁餐厅,你需要的是模具和自动化流水线:
| 高级类型 | 生活类比 |
|---|---|
| 泛型 Generic | "不限菜品的快餐盒"——同一个盒子可以装牛排、可以装沙拉、可以装意面,不变的是"盒子",变的是"内容" |
条件类型 T extends U ? X : Y | "智能开关"——根据输入自动选择输出 |
映射类型 [K in keyof T] | "复印机+滤镜"——把一个对象的所有字段复印一份并加工 |
| infer 关键字 | "侦探"——从复杂类型里推断出某一段 |
| 工具类型 Utility | "官方预制配件",比如 Partial、Pick,开箱即用 |
| 类型体操 | "用类型写代码"——纯类型层面做"编译器编程" |
一句话:高级类型 = 在类型层面写函数。输入是类型,输出还是类型。
1. 泛型(Generic)
1.1 概念是什么
泛型 = 可参数化的类型——把"类型"当成函数的参数传进来。
ts
function identity<T>(x: T): T {
return x;
}
identity<string>("hi"); // T = string,返回 string
identity(42); // 自动推断 T = number类比:泛型函数 = 不限菜品的外卖盒——不管你点什么菜,盒子都能装。
1.2 为什么需要泛型
不用泛型时,你只能:
- 写一个
numberArray,再写一个stringArray……重复劳动 - 用
any,关掉类型检查……失去 IDE 提示
ts
// ❌ 失去类型
function first(arr: any[]): any { return arr[0]; }
const x = first([1, 2, 3]); // x: any
// ✅ 用泛型保留类型
function first<T>(arr: T[]): T { return arr[0]; }
const y = first([1, 2, 3]); // y: number ✅1.3 泛型约束(extends)
让泛型"必须有某个属性":
ts
function getLength<T extends { length: number }>(x: T): number {
return x.length;
}
getLength("hello"); // ✅
getLength([1, 2, 3]); // ✅
getLength({ length: 7 }); // ✅
getLength(123); // ❌ number 没有 length1.4 多泛型 + 默认类型
ts
function pair<A, B = A>(a: A, b: B): [A, B] {
return [a, b];
}
pair(1, "x"); // [number, string]
pair(1, 2); // [number, number]
pair<string>("a"); // ❌ 缺第二个参数1.5 泛型接口 / 泛型类
ts
interface ApiResp<T> {
code: number;
data: T;
}
const r: ApiResp<{ id: number }> = { code: 0, data: { id: 1 } };
class Stack<T> {
private items: T[] = [];
push(x: T) { this.items.push(x); }
pop(): T | undefined { return this.items.pop(); }
}
const s = new Stack<number>();
s.push(1); s.pop(); // returns number | undefined2. 条件类型 + infer
2.1 条件类型:类型版的三元
ts
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<"hello">; // "yes"
type B = IsString<123>; // "no"类比:T extends U ? X : Y = 智能开关——T 像 U 就走 X,不像就走 Y。
2.2 分发条件类型
当 T 是裸类型参数(没有被包成元组等),联合会"自动分发":
ts
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>;
// 等价于 ToArray<string> | ToArray<number>
// = string[] | number[]想关闭分发,把 T 包成元组即可:
ts
type ToArray2<T> = [T] extends [any] ? T[] : never;
type R2 = ToArray2<string | number>; // (string | number)[]2.3 infer:类型层面的"模式匹配"
infer X = "让 TS 帮我把这一段类型起个名叫 X"。
ts
// 取数组元素类型
type ElementOf<T> = T extends (infer U)[] ? U : never;
type N = ElementOf<number[]>; // number
// 取函数返回值
type ReturnTypeX<T> = T extends (...args: any) => infer R ? R : never;
type FR = ReturnTypeX<() => string>; // string
// 取 Promise 内部类型
type Awaited2<T> = T extends Promise<infer U> ? U : T;
type AW = Awaited2<Promise<number>>; // numberASCII 图——条件类型流程:
T 是数组吗?
│
┌───────┴────────┐
│ T extends (infer U)[] │
▼ Yes │ No
返回 U │
▼
返回 never3. 映射类型(Mapped Types)
一句话:遍历一个类型的所有 key,生成一个新类型。
3.1 基本写法 [K in keyof T]
ts
type Mutable<T> = { [K in keyof T]: T[K] };
// 等价于复制原来的所有字段3.2 加修饰符:? readonly 与 -? -readonly
ts
type MyPartial<T> = { [K in keyof T]?: T[K] }; // 全部可选
type MyRequired<T> = { [K in keyof T]-?: T[K] }; // 去掉可选
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };3.3 重映射 key(as)
ts
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person { name: string; age: number }
type PG = Getters<Person>;
// {
// getName: () => string;
// getAge: () => number;
// }类比:映射类型 = 复印机+滤镜——一边复印,一边给每个 key/value 上一层加工。
4. 工具类型大全
TS 内置的"官方预制配件",几乎天天用:
| 工具 | 作用 | 输入 | 输出 |
|---|---|---|---|
Partial<T> | 全部字段可选 | { a: 1; b: 2 } | { a?; b? } |
Required<T> | 全部字段必填 | { a?; b? } | { a; b } |
Readonly<T> | 全部字段只读 | { a } | { readonly a } |
Pick<T, K> | 挑出指定字段 | { a; b; c }, "a" | "b" | { a; b } |
Omit<T, K> | 去掉指定字段 | { a; b; c }, "c" | { a; b } |
Record<K, V> | 构造索引对象 | "x" | "y", number | { x: number; y: number } |
Exclude<U, V> | 联合中剔除 | "a" | "b" | "c", "a" | "b" | "c" |
Extract<U, V> | 联合中保留 | "a" | "b" | "c", "a" | "c" | "a" | "c" |
NonNullable<T> | 去掉 null/undefined | string | null | string |
ReturnType<F> | 取函数返回类型 | () => number | number |
Parameters<F> | 取函数参数元组 | (a: 1, b: 2) => any | [1, 2] |
Awaited<T> | 拆 Promise(递归) | Promise<Promise<number>> | number |
Uppercase<S> | 字符串大写 | "abc" | "ABC" |
Capitalize<S> | 首字母大写 | "abc" | "Abc" |
4.1 实战:业务表单的常用组合
ts
interface User {
id: number;
name: string;
email: string;
password: string;
}
// 创建用户的入参:不要 id(后端生成),password 必填
type CreateUserDTO = Omit<User, "id">;
// 更新用户:所有字段可选,但 id 必须有
type UpdateUserDTO = Partial<User> & { id: number };
// 对外返回:去掉密码
type PublicUser = Omit<User, "password">;
// 字典:以用户 id 索引完整对象
type UserMap = Record<number, PublicUser>;5. 类型体操入门
5.1 手写 MyPartial
ts
type MyPartial<T> = { [K in keyof T]?: T[K] };
type T1 = MyPartial<{ a: 1; b: 2 }>; // { a?: 1; b?: 2 }5.2 手写 MyPick
ts
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type T2 = MyPick<{ a: 1; b: 2; c: 3 }, "a" | "c">; // { a:1; c:3 }5.3 手写 MyExclude
ts
type MyExclude<T, U> = T extends U ? never : T;
type T3 = MyExclude<"a" | "b" | "c", "a">; // "b" | "c"关键:联合分发 + 条件类型,命中 U 的成员变成
never,自然消失。
5.4 手写 MyReturnType
ts
type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type T4 = MyReturnType<() => string>; // string5.5 手写 DeepReadonly(深只读)
ts
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function ? T[K] : DeepReadonly<T[K]>
: T[K];
};
type Cfg = DeepReadonly<{ a: { b: { c: number } } }>;
// 三层 readonly,全部字段都不能修改类比:类型体操 = 用类型积木拼电路——电路本身不"运行",但它会指挥真正的代码。
6. 模块声明 / d.ts 文件
6.1 d.ts 是什么
.d.ts 文件 = 只有类型、没有实现的声明文件。让 TS 知道某段 JS 代码"长什么样"。
6.2 常见三种用途
① 给纯 JS 库写类型
ts
// types/lodash-shim.d.ts
declare module "lodash-shim" {
export function chunk<T>(arr: T[], size: number): T[][];
}② 全局类型增强
ts
// types/global.d.ts
declare global {
interface Window {
__APP_VERSION__: string;
}
}
export {};③ 资源文件类型(Webpack/Vite)
ts
// types/assets.d.ts
declare module "*.svg" {
const url: string;
export default url;
}
declare module "*.css";6.3 让 TS 找到 d.ts
tsconfig.json:
json
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./types"]
}
}7. ⚠️ 易踩的坑
- 滥用泛型:当函数只用一种类型时,不要为了"显得专业"加泛型——会让签名更复杂。
- 条件类型分发陷阱:联合类型在裸类型参数下会自动分发,常常不是你想要的。要禁用分发就包成元组:
[T] extends [U]。 keyof any=string | number | symbol,不是你想象的"所有可能的 key"。Partial<T>是浅可选:嵌套对象内层不变,要"深可选"得自己写DeepPartial。Readonly<T>也是浅只读:内层数组依然可 push。infer必须配合extends:单独写infer R没意义。- 类型体操别炫技:业务代码里能用工具类型解决就别手撸;除非真有复杂推断需求。
- d.ts 里写
declare module "xxx"后整个项目共享:注意命名冲突,必要时放namespace。
8. 一句话总结
TS 的高级类型让你在类型系统里"再写一遍代码"——一次定义,处处复用,用编译器把重复的工作全干完。
9. 延伸阅读
- 类型体操题库:type-challenges
- 官方手册:TS Handbook · Generics
- 工具类型源码:lib.es5.d.ts
- 进阶博客:Matt Pocock 的 TS 教学