ReplaceKeys
ReplaceKeys
题目
实现类型 ReplaceKeys,在联合类型中替换指定的键。如果联合中的某个类型没有该键,则跳过替换。该类型接收三个参数。
type NodeA = {
type: 'A'
name: string
flag: number
}
type NodeB = {
type: 'B'
id: number
flag: number
}
type NodeC = {
type: 'C'
name: string
flag: number
}
type Nodes = NodeA | NodeB | NodeC
type ReplacedNodes = ReplaceKeys<
Nodes,
'name' | 'flag',
{ name: number; flag: string }
>
// {type: 'A', name: number, flag: string}
// | {type: 'B', id: number, flag: string}
// | {type: 'C', name: number, flag: string}
type ReplacedNotExistKeys = ReplaceKeys<Nodes, 'name', { aa: number }>
// {type: 'A', name: never, flag: number}
// | NodeB
// | {type: 'C', name: never, flag: number}解法
type ReplaceKeys<U, T, Y> = U extends any
? {
[K in keyof U]: K extends T
? K extends keyof Y
? Y[K]
: never
: U[K]
}
: never深入理解
问题拆解:跨联合类型替换键
我们有联合类型 U、待替换的键集合 T(字符串字面量联合),以及替换映射 Y。对联合中的每个成员,我们需要遍历其所有键,将出现在 T 中的键替换为 Y 中对应的类型。
难点在于:
- 键可能存在于替换映射
Y中 —— 使用Y[K]; - 键可能不在
Y中(比如你传了{ aa: number }来替换name)—— 返回never; - 键根本不在
T中 —— 保留原类型U[K]; - 某些联合成员可能根本没有
T中的键 —— 直接跳过,不做修改。
分发联合类型
核心技巧是 U extends any。这看起来毫无意义——所有类型都 extends any——但它触发了分发条件类型(distributive conditional types)。
当 U 是联合类型 NodeA | NodeB | NodeC 时,写 U extends any ? ... : ... 会让 TypeScript 将联合拆开,逐个对每个成员应用条件:
// 概念上等价于:
(NodeA extends any ? Mapped<NodeA> : never)
| (NodeB extends any ? Mapped<NodeB> : never)
| (NodeC extends any ? Mapped<NodeC> : never)这正是我们需要的——每个成员被独立处理,某成员中不存在的键自然不会出现在结果中。
内部的映射类型
分发后,我们对单个联合成员 U 的键进行映射:
{
[K in keyof U]: K extends T
? K extends keyof Y
? Y[K]
: never
: U[K]
}逐分支分析:
| 条件 | 含义 | 结果 |
|---|---|---|
K extends T 为 false | 该键不需要替换 | U[K],保留原类型 |
K extends T 为 true 且 K extends keyof Y 为 true | 键在 T 中,且 Y 有对应替换 | Y[K],使用新类型 |
K extends T 为 true 且 K extends keyof Y 为 false | 键在 T 中,但 Y 没有定义它 | never |
最后这个 never 分支正好对应题目要求:ReplaceKeys<Nodes, 'name', { aa: number }> 中 name 应变为 never,因为 Y 里只有 aa,没有 name。
为什么不直接映射 T?
你可能想直接 [K in T] 来映射待替换的键。但这样会丢失所有不在 T 中的键。我们需要遍历 keyof U(当前成员的所有键),只对其中属于 T 的键做替换。
分发的重要性
如果不用 U extends any 触发分发,直接写:
// ❌ 不分发的版本
type ReplaceKeys<U, T, Y> = {
[K in keyof U]: K extends T ? (K extends keyof Y ? Y[K] : never) : U[K]
}对联合类型 U = NodeA | NodeB | NodeC,keyof U 会变成所有成员键的交集——只保留每个成员都有的键(type | flag)。成员独有的键如 name、id 就全部丢失了。
有了分发,我们分别对 keyof NodeA、keyof NodeB、keyof NodeC 进行映射,每个成员的完整键都能被正确保留。
分发条件类型速查
| 写法 | 效果 |
|---|---|
T extends SomeType ? A : B | 若 T 是裸类型参数则分发 |
T extends any ? A : B | 强制分发(常用技巧) |
[T] extends [SomeType] ? A : B | 不分发(元组包裹) |
T extends never ? A : B | never 输入时直接返回 never |
核心要点
U extends any是强制分发联合类型的标准技巧——每个成员被独立处理- 映射
keyof U而非T——这样才能保留所有原始键,只替换目标键 - 三路键判断:不在
T中 → 保留原类型;在T且在Y中 → 用Y[K];在T但不在Y中 →never - 分发条件类型是 TypeScript 中"逐成员"联合类型变换的基础
U extends any ? { ... } : never这个模式在高级工具类型中随处可见
