类型层面编程:映射类型、条件类型与内置工具
需求场景
订单系统的类型定义越来越多。你发现很多类型之间存在明显的派生关系:
Order有 10 个字段,更新接口只需要传部分字段(所有字段变可选)Order的列表查询需要一个只包含orderId、status、totalAmount三个字段的精简版- 后端返回的所有字段都是只读的,前端表单编辑时需要把只读去掉
- 事件处理器的名称由事件名自动派生:
click对应onClick,submit对应onSubmit
手动为每种变体写一遍类型定义既冗长又容易出错。TypeScript 提供了一套在类型层面编程的能力——映射类型、条件类型和 infer 推断——让你可以从已有类型自动派生出新类型。
keyof 运算符
keyof 是类型层面编程的基础工具,它提取对象类型的所有属性名,返回一个联合类型:
type OrderFields = {
orderId: string;
customerId: string;
totalAmount: number;
status: string;
};
type FieldNames = keyof OrderFields;
// "orderId" | "customerId" | "totalAmount" | "status"
带索引签名的类型中,keyof 返回索引的类型:
interface DynamicMap {
[prop: string]: number;
}
type MapKeys = keyof DynamicMap; // string | number
// 返回 string | number 是因为 JavaScript 中数字索引本质上也是字符串
keyof 的实际用途
编写类型安全的属性访问函数:
function readField<TObj, TKey extends keyof TObj>(
source: TObj,
field: TKey
): TObj[TKey] {
return source[field];
}
const order = { orderId: "ORD-001", totalAmount: 599, status: "paid" };
const id = readField(order, "orderId"); // string
const amount = readField(order, "totalAmount"); // number
// readField(order, "nonExistent"); // 编译错误
keyof 与联合类型、交叉类型
interface SectionA { title: string; score: number }
interface SectionB { score: number; grade: string }
// 联合类型:keyof 返回共有键
type SharedKeys = keyof (SectionA | SectionB); // "score"
// 交叉类型:keyof 返回所有键
type AllKeys = keyof (SectionA & SectionB); // "title" | "score" | "grade"
typeof 在类型层面的使用
在类型上下文中,typeof 从一个运行时值反推出其类型:
const shippingConfig = {
freeThreshold: 99,
baseFee: 10,
expressMultiplier: 1.5,
regions: ["CN", "HK", "TW"],
};
type ShippingConfig = typeof shippingConfig;
// {
// freeThreshold: number;
// baseFee: number;
// expressMultiplier: number;
// regions: string[];
// }
typeof 与 keyof 的经典组合:
const STATUS_MAP = {
pending: "待处理",
processing: "处理中",
completed: "已完成",
cancelled: "已取消",
};
type StatusCode = keyof typeof STATUS_MAP;
// "pending" | "processing" | "completed" | "cancelled"
从 as const 数组中提取元素类型:
const supportedCurrencies = ["CNY", "USD", "EUR", "GBP"] as const;
type Currency = (typeof supportedCurrencies)[number];
// "CNY" | "USD" | "EUR" | "GBP"
索引访问类型
用方括号从对象类型中提取属性的值类型:
type OrderDetail = {
orderId: string;
amount: number;
items: {
sku: string;
qty: number;
unitPrice: number;
}[];
};
type AmountType = OrderDetail["amount"]; // number
type ItemType = OrderDetail["items"][number]; // { sku: string; qty: number; unitPrice: number }
type SkuType = OrderDetail["items"][number]["sku"]; // string
传入联合类型作为索引,一次提取多个属性类型:
type IdOrAmount = OrderDetail["orderId" | "amount"]; // string | number
结合 keyof 获取所有属性值的联合类型:
type AllValueTypes = OrderDetail[keyof OrderDetail];
// string | number | { sku: string; qty: number; unitPrice: number }[]
映射类型
映射类型是类型层面的 map 操作,基于已有类型的结构生成新类型。
基本语法
type MappedType = {
[Key in Keys]: ValueType;
};
Key 是属性名变量,in 遍历联合类型的每个成员。
示例
复制类型的所有属性:
type OrderSnapshot = {
orderId: string;
status: string;
amount: number;
};
type ClonedSnapshot = {
[K in keyof OrderSnapshot]: OrderSnapshot[K];
};
// { orderId: string; status: string; amount: number }
统一修改值类型:
type BooleanFlags<T> = {
[K in keyof T]: boolean;
};
type OrderFlags = BooleanFlags<OrderSnapshot>;
// { orderId: boolean; status: boolean; amount: boolean }
从联合类型生成对象:
type StatusObject = {
[S in "pending" | "paid" | "shipped"]: boolean;
};
// { pending: boolean; paid: boolean; shipped: boolean }
映射修饰符
用 + 和 - 添加或移除 readonly 和 ? 修饰符:
// 全部变可选
type AllOptional<T> = {
[K in keyof T]+?: T[K];
};
// 全部变必填(去掉可选标记)
type AllRequired<T> = {
[K in keyof T]-?: T[K];
};
// 全部变只读
type Frozen<T> = {
+readonly [K in keyof T]: T[K];
};
// 全部变可写(去掉只读标记)
type Unfrozen<T> = {
-readonly [K in keyof T]: T[K];
};
+ 是默认行为可以省略。readonly 等价于 +readonly,? 等价于 +?。
键重映射(as 子句)
TypeScript 4.1 引入的 as 子句,在映射过程中重命名属性:
type OrderFields = {
orderId: string;
status: string;
amount: number;
};
// 给每个属性名添加后缀
type FieldValidators = {
[K in keyof OrderFields as `validate${Capitalize<K & string>}`]: () => boolean;
};
// {
// validateOrderId: () => boolean;
// validateStatus: () => boolean;
// validateAmount: () => boolean;
// }
经典的 Getter 生成模式:
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
type OrderGetters = Getters<OrderFields>;
// {
// getOrderId: () => string;
// getStatus: () => string;
// getAmount: () => number;
// }
属性过滤
通过在键重映射中返回 never 过滤属性:
// 只保留值类型为 number 的属性
type NumericFields<T> = {
[K in keyof T as T[K] extends number ? K : never]: T[K];
};
type OrderNumerics = NumericFields<OrderFields>;
// { amount: number }
条件类型
条件类型是类型层面的三元运算符:
T extends U ? X : Y
如果 T 能赋值给 U,结果为 X,否则为 Y。
基础示例
type IsNumeric<T> = T extends number ? true : false;
type A = IsNumeric<42>; // true
type B = IsNumeric<string>; // false
type C = IsNumeric<3.14>; // true
分布式条件类型
当条件类型作用于泛型的联合类型参数时,自动对每个成员分别运算:
type WrapInArray<T> = T extends any ? T[] : never;
type Result = WrapInArray<string | number>;
// string[] | number[](分别对 string 和 number 应用)
不想要分布式行为时,用方括号包裹:
type WrapInArray<T> = [T] extends [any] ? T[] : never;
type Result = WrapInArray<string | number>;
// (string | number)[]
过滤联合类型
type FilterOut<T, U> = T extends U ? never : T;
type Remaining = FilterOut<"a" | "b" | "c" | "d", "a" | "c">;
// "b" | "d"
// 分布式展开过程:
// "a" extends "a"|"c" ? never : "a" => never
// "b" extends "a"|"c" ? never : "b" => "b"
// "c" extends "a"|"c" ? never : "c" => never
// "d" extends "a"|"c" ? never : "d" => "d"
// never | "b" | never | "d" => "b" | "d"
infer 关键字
infer 在条件类型中声明待推断的类型变量,让编译器自动推导:
// 提取数组元素类型
type ItemOf<T> = T extends Array<infer Elem> ? Elem : T;
type A = ItemOf<string[]>; // string
type B = ItemOf<number[]>; // number
type C = ItemOf<boolean>; // boolean(非数组,原样返回)
推断函数类型
// 提取函数返回类型
type ResultOf<T> = T extends (...args: any[]) => infer R ? R : never;
type X = ResultOf<() => string>; // string
type Y = ResultOf<(a: number) => void>; // void
// 提取函数参数类型
type ArgsOf<T> = T extends (...args: infer P) => any ? P : never;
type Z = ArgsOf<(a: string, b: number) => void>; // [a: string, b: number]
推断模板字面量
type ExtractPath<T> = T extends `${string}://${string}/${infer Path}`
? Path
: never;
type P1 = ExtractPath<"https://api.example.com/orders/list">;
// "orders/list"
模板字面量类型
模板字面量类型在类型层面做字符串拼接:
type Module = "order" | "product" | "user";
type Action = "Create" | "Update" | "Delete";
type EventName = `${Module}${Action}`;
// "orderCreate" | "orderUpdate" | "orderDelete"
// | "productCreate" | "productUpdate" | "productDelete"
// | "userCreate" | "userUpdate" | "userDelete"
联合类型自动做笛卡尔积展开:
type Row = "A" | "B";
type Col = "1" | "2" | "3";
type Cell = `${Row}${Col}`;
// "A1" | "A2" | "A3" | "B1" | "B2" | "B3"
内置工具类型详解
TypeScript 内置了一系列工具类型,都基于映射类型、条件类型和 infer 实现。
Partial
全部属性变可选:
interface OrderDraft {
customerId: string;
items: string[];
coupon: string;
}
type OptionalDraft = Partial<OrderDraft>;
// { customerId?: string; items?: string[]; coupon?: string }
// 实现原理
type Partial<T> = { [K in keyof T]?: T[K] };
典型用途:更新操作只传部分字段。
function updateDraft(id: string, changes: Partial<OrderDraft>): void {
console.log(`Updating ${id}`, changes);
}
updateDraft("D-001", { coupon: "SAVE20" });
Required
全部属性变必填:
interface OptionalConfig {
host?: string;
port?: number;
debug?: boolean;
}
type FullConfig = Required<OptionalConfig>;
// { host: string; port: number; debug: boolean }
// 实现原理
type Required<T> = { [K in keyof T]-?: T[K] };
Readonly
全部属性变只读:
interface TaskItem {
title: string;
done: boolean;
}
type FrozenTask = Readonly<TaskItem>;
// { readonly title: string; readonly done: boolean }
// 实现原理
type Readonly<T> = { readonly [K in keyof T]: T[K] };
Pick<T, K>
选取指定属性:
interface FullOrder {
orderId: string;
customerId: string;
totalAmount: number;
status: string;
remark: string;
}
type OrderBrief = Pick<FullOrder, "orderId" | "status" | "totalAmount">;
// { orderId: string; status: string; totalAmount: number }
// 实现原理
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
Omit<T, K>
排除指定属性:
type OrderWithoutRemark = Omit<FullOrder, "remark">;
// { orderId: string; customerId: string; totalAmount: number; status: string }
// 实现原理
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Record<K, T>
构造键值对对象类型:
type StatusLabel = Record<"pending" | "paid" | "shipped", string>;
// {
// pending: string;
// paid: string;
// shipped: string;
// }
// 实现原理
type Record<K extends string | number | symbol, T> = { [P in K]: T };
Exclude<T, U>
从联合类型中排除成员:
type AllStatus = "pending" | "paid" | "shipped" | "cancelled";
type ActiveStatus = Exclude<AllStatus, "cancelled">;
// "pending" | "paid" | "shipped"
// 实现原理
type Exclude<T, U> = T extends U ? never : T;
Extract<T, U>
从联合类型中提取成员:
type TerminalStatus = Extract<AllStatus, "shipped" | "cancelled">;
// "shipped" | "cancelled"
// 实现原理
type Extract<T, U> = T extends U ? T : never;
NonNullable
排除 null 和 undefined:
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string
// 实现原理(TypeScript 4.8+ 的实现方式)
type NonNullable<T> = T & {};
历史注记:TypeScript 4.8 之前,
NonNullable的实现为T extends null | undefined ? never : T(基于条件类型)。4.8 改用交叉类型T & {}后行为更一致——它不再触发分布式条件类型展开,对泛型参数的过滤也更可预测。你在网上看到的很多资料仍在使用旧版实现,但两者在非泛型场景下的结果完全一致。
ReturnType
提取函数返回类型:
function calculateDiscount(price: number, rate: number) {
return { discounted: price * (1 - rate), saved: price * rate };
}
type DiscountResult = ReturnType<typeof calculateDiscount>;
// { discounted: number; saved: number }
// 实现原理
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
Parameters
以元组形式返回函数参数类型:
type DiscountArgs = Parameters<typeof calculateDiscount>;
// [price: number, rate: number]
// 实现原理
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
InstanceType
提取构造函数的实例类型:
type ErrInstance = InstanceType<ErrorConstructor>; // Error
type RegInstance = InstanceType<RegExpConstructor>; // RegExp
// 实现原理
type InstanceType<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer R ? R : any;
字符串操作工具
type U = Uppercase<"hello">; // "HELLO"
type L = Lowercase<"HELLO">; // "hello"
type C = Capitalize<"hello">; // "Hello"
type D = Uncapitalize<"Hello">; // "hello"
Awaited
递归解包 Promise 的值类型:
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number
type C = Awaited<boolean | Promise<string>>; // boolean | string
综合实战:自动生成事件处理器类型
interface OrderEventMap {
submit: { orderId: string; items: string[] };
cancel: { orderId: string; reason: string };
refund: { orderId: string; amount: number };
}
// 生成事件处理器方法名
type HandlerName<K extends string> = `on${Capitalize<K>}`;
// 生成完整的事件处理器接口
type EventHandlers<TMap extends Record<string, any>> = {
[K in keyof TMap as HandlerName<K & string>]: (event: TMap[K]) => void;
};
type OrderHandlers = EventHandlers<OrderEventMap>;
// {
// onSubmit: (event: { orderId: string; items: string[] }) => void;
// onCancel: (event: { orderId: string; reason: string }) => void;
// onRefund: (event: { orderId: string; amount: number }) => void;
// }
// 提取特定事件的数据类型
type SubmitData = OrderEventMap["submit"];
// { orderId: string; items: string[] }
// 只保留包含 orderId 的事件(实际上全部都有,这里演示过滤能力)
type OrderIdEvents<TMap extends Record<string, any>> = {
[K in keyof TMap as TMap[K] extends { orderId: string } ? K : never]: TMap[K];
};
type FilteredEvents = OrderIdEvents<OrderEventMap>;
// 保留所有三个事件(因为都有 orderId)
本章回顾
| 概念 | 语法 | 用途 |
|---|---|---|
| keyof | keyof T | 提取对象的所有属性名 |
| typeof(类型) | typeof value | 从值反推类型 |
| 索引访问 | T["key"] | 提取属性的值类型 |
| 映射类型 | [K in Keys]: T | 从现有类型结构派生出变体类型 |
| 映射修饰符 | +? / -? / +readonly / -readonly | 添加或移除修饰符 |
| 键重映射 | as NewKey | 映射时重命名属性名 |
| 条件类型 | T extends U ? X : Y | 类型层面的三元运算 |
| 分布式条件 | 联合类型自动展开 | 对每个成员分别运算 |
| infer | infer R | 在条件类型中推断类型 |
| 模板字面量 | `${A}${B}` | 类型层面的字符串拼接 |
| Partial | Partial<T> | 全部属性变可选 |
| Required | Required<T> | 全部属性变必填 |
| Readonly | Readonly<T> | 全部属性变只读 |
| Pick / Omit | Pick<T, K> | 选取/排除属性 |
| Record | Record<K, T> | 构造键值对类型 |
| Exclude / Extract | Exclude<T, U> | 联合类型的过滤 |
| ReturnType | ReturnType<T> | 提取函数返回类型 |
| Parameters | Parameters<T> | 提取函数参数类型 |
购买课程解锁全部内容
告别类型错误:12 章掌握 TypeScript 工程实战
¥29.90