[K in keyof T] 是类型层的 for 循环: 遍历键、改形状、换名字 — Partial/Pick/Omit 全是它的马甲
映射类型是类型层的 for 循环: [K in keyof T] 把 T 的每个键拎出来遍历一遍, 每个键生成一个新属性, 值你想怎么映射就怎么映射(T[K] 原样、boolean 全换成、Promise<T[K]> 全包上)。官方的 Partial/Pick/Omit/Record/Readonly 没有一个是"内置魔法", 全部是几行映射类型的公开实现。
as 重映射再给循环加一步"改名+过滤": 模板字面量把 name 拼成 getName, as never 把不要的键扔掉。掌握这三块积木 — 键集合、值映射、as 变形 — 你就能自造团队需要的任何工具类型, 而不是到处抄 200 行的 DeepXxx。
type K = keyof { a: 1; b: 2 }; // → 'a' | 'b' type AK = keyof number[]; // → number | 'length' | 方法名...
type U = { a: string; b: number }['a' | 'b']; U; // → string | number 键联合带出值联合
const roles = ['admin', 'user'] as const; type Role = (typeof roles)[number]; // → 'admin' | 'user'
type Flags = { [K in 'read' | 'write']: boolean }; Flags; // → { read: boolean; write: boolean }
// 去 ?: [K in keyof T]-?: T[K] // 去 readonly: -readonly [K in keyof T]: T[K]
type StripIds<T> = { [K in keyof T as K extends 'id' ? never : K]: T[K]; };
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
type Pub = Pick<User, 'id' | 'name'>; type Safe = Omit<User, 'password'>;
type Handlers = Record<EventType, (p: never) => void>; const h: Record<'a' | 'b', number> = { a: 1, b: 2 };
type R = ReturnType<typeof loadUser>; // → Promise<User> type U = Awaited<R>; // → User
type Route = `/${string}/edit`; const r: Route = '/users/edit'; // ✓ const bad: Route = '/users'; // ✗ 编译错
const table = { a: { v: 1 }, b: { v: 2 }, } satisfies Record<string, { v: number }>; table.a.v; // 保留具体结构, 不是 index 签名黑洞
"改昵称"也要传完整用户对象是老接口的痛。浅 Partial + 自定义深 Partial 分层覆盖:
type UserPatch = Partial<Pick<User, 'name' | 'avatar'>>; type DeepPatch<T> = { [K in keyof T]?: T[K] extends object ? DeepPatch<T[K]> : T[K] }; const body: DeepPatch<Settings> = { notify: { email: false }, // 只发一层子树, 其余不动 }; await api.patch('/settings', body);
emit('pay', wrongPayload) 静默通过。键映射让每个事件名锁定自己的载荷:
type EventMap = { pay: { orderId: string; cents: number }; login: { userId: string }; }; class Emitter { on<K extends keyof EventMap>(e: K, cb: (p: EventMap[K]) => void) {} emit<K extends keyof EventMap>(e: K, p: EventMap[K]) {} } emitter.emit('pay', { orderId: '1', cents: 100 }); // ✓
路径字符串里的 :id 部分被忽略, params 拼错没人管。模板字面量 + infer 从路径提取参数键:
type ParamKeys<S extends string> = S extends `${string:}${infer P}/`${string}` | `${string:}${infer P}` ? P | ParamKeys<S> : never; type Route = '/users/:userId/orders/:orderId'; type Keys = ParamKeys<Route>; // → 'userId' | 'orderId' type Params = Record<Keys, string>;
字段名在 interface、校验器、初始值三处手写, 永远漂移。映射类型让三处同源:
type Fields = { email: string; age: number }; const initial: Fields = { email: '', age: 0 }; type Rules = { [K in keyof Fields]: (v: Fields[K]) => string | null }; const rules: Rules = { email: v => v.includes('@') ? null : '邮箱非法', age: v => v >= 0 ? null : '年龄非法', }; // Fields 加字段 → initial 与 rules 编译期双双报缺
switch 漏 case 静默, Record 直接强迫每个键都配值:
type Level = 'debug' | 'info' | 'error'; const color: Record<Level, string> = { debug: 'gray', info: 'cyan', error: 'red', }; // 少写 'error' → 编译错, 而不是运行时 undefined
出参 DTO 与实体手工各一份。映射派生: 排除敏感字段 + 日期转字符串:
type Serialize<T> = { [K in keyof T as K extends 'password' | 'salt' ? never : K]: T[K] extends Date ? string : T[K]; }; type UserDTO = Serialize<User>; // 自动: 无敏感字段, 日期变 string
主题系统只允许覆盖指定子集且全部可选, 类型即文档:
type ThemeOverrides = Partial<Pick<Theme, 'primary' | 'radius' | 'font'>>; const t: ThemeOverrides = { primary: '#22d3ee' }; setTheme(t); // 只能覆盖白名单字段, 其余编译报错
给任意 handler 加日志, 不想丢签名。类型工具反推参数与返回:
function withLog<F extends (...a: any[]) => unknown>(fn: F) { return (...args: Parameters<F>): ReturnType<F> => { logger.info('call', { name: fn.name }); return fn(...args) as ReturnType<F>; }; } const handler2 = withLog(handler); // 签名分毫不差
插件列表 (A|B|C)[] 想得到 A&B&C 的合并能力, infer 逆变位捕获:
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; type Caps = UnionToIntersection<CapA | CapB>; // → CapA & CapB function useAll(...ps: Caps[]) { ps[0].sharedMethod(); }
t('user.profile.name') 拼错不报错。递归模板字面量生成全部合法路径:
type Paths<T, P extends string = ''> = T extends object ? { [K in keyof T]: Paths<T[K], P extends '' ? K & string : `${P}.${K & string}` }[keyof T] : P; type Key = Paths<{ user: { name: string } }>; // 'user.name' t('user.nmae'); // ✗ 编译期抓出拼写错误
// 错: Partial<Cfg> 里 cfg.db.host 仍必填 // 对: DeepPartial<Cfg> 递归映射每层
type StrictOmit<T, K extends keyof T> = Omit<T, K>;
type E = (typeof arr)[number]; // 元素类型 ✓
// 错: const m: Record<string, number> = { prot: 8080 } // typo 放行 // 对: satisfies Record<string, number> 保留字面量并给提示
// 调试: type _ = Show<MyMapped>; 用编辑器悬停看中间态
// 对外暴露配置: readonly 类型 + Object.freeze 双保险// 重载函数的 ReturnType ≠ 所有分支的并type Eq<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
Pick<User, 'emial'>; // 可能只告警 StrictPick<User, 'emial'>; // ✓ 编译错
type X = { id: number } & { id: string }; X['id']; // → never id 永远赋不了值
// 需要"全部必填的映射": [K in keyof T]-?: NewVal<T[K]>
// 错: `${A|B}${C|D}${E|F}` 三层联合直接笛卡尔积// 错: { name: string; [k: string]: number } // name 被同化报错
type T = User; // ✓ // type T = typeof User; ✗ User 是类型不是值
// 对: type Step1 = ...; type Step2 = ...; 命名中间态
const L = { debug: 'debug' } as const; type LK = keyof typeof L; // 'debug' 干净
type IsNever<T> = [T] extends [never] ? true : false;
// 具名映射保留字面量键信息, 悬停可见每键// 键顺序 = keyof 顺序, 仅展示用, 不构成契约// 自造工具三件套: keyof + in + as, 加测试型 Expect<Equal<>>