Yuzhe's Blog

yuzhes

Merge

Merge

题目链接

题目

将两个类型合并成一个新类型,第二个类型的键会覆盖第一个类型的同名键。

type foo = {
  name: string
  age: string
}
type coo = {
  age: number
  sex: string
}

type Result = Merge<foo, coo>
// 期望结果:{ name: string; age: number; sex: string }

解答

type Merge<F, S> = {
  [K in keyof F | keyof S]: K extends keyof S ? S[K] : K extends keyof F ? F[K] : never
}

核心思路:遍历两个类型所有键的联合,对每个键判断应该取哪个类型的值——第二个类型 S 的优先级更高。

逐步拆解

第一步 — 收集所有键:keyof F | keyof S

映射类型遍历 FS 中出现的每一个键,确保不会丢失任何属性。

第二步 — 通过嵌套条件确定优先级

对于每个键 K

  1. 如果 KS 的键 → 使用 S[K](第二个类型优先)
  2. 否则如果 KF 的键 → 使用 F[K](保留第一个类型的值)
  3. 否则 → never(实际上不可达,因为 K 必来自两者之一)

用例子走一遍:

结果:{ name: string; age: number; sex: string }

深入理解

另一种解法:交叉类型

另一个常见解法是用 Omit + 交叉类型:

type Merge<F, S> = Omit<F, keyof S> & S

原理:

  1. F 中删除所有在 S 中存在的键
  2. 将剩余部分与 S 做交叉

写法简洁,但结果类型是交叉类型Omit<...> & S),不是纯粹的对象字面量类型。IDE 工具提示通常会把它展示成合并后的对象,但类型结构上是不同的。映射类型方案则直接产生干净、扁平的对象类型。

如果想”压平”交叉类型,可以再套一层映射类型:

type Merge<F, S> = {
  [K in keyof (Omit<F, keyof S> & S)]: (Omit<F, keyof S> & S)[K]
}

不过这样一来,还不如直接用最开始的映射类型解法清晰。

为什么 keyof F | keyof S 在映射类型里有效

TypeScript 允许在映射类型的 keyof 位置使用联合类型:

type Keys = keyof { a: 1 } | keyof { b: 2 }  // 'a' | 'b'

type M = {
  [K in 'a' | 'b']: ...  // 遍历两个键
}

这是众多”合并两个对象”类型操作的基础写法。

never 分支的意义

K extends keyof S ? S[K] : K extends keyof F ? F[K] : never

最后的 never 在实践中永远不会触达。因为 K 被约束为 keyof F | keyof S,它一定满足前两个条件之一。这个分支的存在只是为了满足 TypeScript 类型检查器——条件类型的每个分支都必须有合法的类型。

映射类型 vs 工具类型对比

方案结果形状可读性
直接映射类型扁平对象✅ 清晰明确
Omit<F, keyof S> & S交叉类型⚠️ 隐式压平
{...(Omit<F,keyof S> & S)}扁平对象🔄 两步操作

在实际业务代码中,Omit & S 是惯用写法。在类型体操中,映射类型方案更能体现原理。

核心要点