Yuzhe's Blog

yuzhes

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 中对应的类型。

难点在于:

  1. 键可能存在于替换映射 Y 中 —— 使用 Y[K]
  2. 键可能不在 Y 中(比如你传了 { aa: number } 来替换 name)—— 返回 never
  3. 键根本不在 T 中 —— 保留原类型 U[K]
  4. 某些联合成员可能根本没有 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 Tfalse该键不需要替换U[K],保留原类型
K extends TtrueK extends keyof Ytrue键在 T 中,且 Y 有对应替换Y[K],使用新类型
K extends TtrueK extends keyof Yfalse键在 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 | NodeCkeyof U 会变成所有成员键的交集——只保留每个成员都有的键(type | flag)。成员独有的键如 nameid 就全部丢失了。

有了分发,我们分别对 keyof NodeAkeyof NodeBkeyof NodeC 进行映射,每个成员的完整键都能被正确保留。

分发条件类型速查

写法效果
T extends SomeType ? A : BT 是裸类型参数则分发
T extends any ? A : B强制分发(常用技巧)
[T] extends [SomeType] ? A : B分发(元组包裹)
T extends never ? A : Bnever 输入时直接返回 never

核心要点