Merge
大约 3 分钟
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
映射类型遍历 F 和 S 中出现的每一个键,确保不会丢失任何属性。
第二步 — 通过嵌套条件确定优先级
对于每个键 K:
- 如果
K是S的键 → 使用S[K](第二个类型优先) - 否则如果
K是F的键 → 使用F[K](保留第一个类型的值) - 否则 →
never(实际上不可达,因为K必来自两者之一)
用例子走一遍:
name:只在F中 →stringage:两者都有,但S优先 →numbersex:只在S中 →string
结果:{ name: string; age: number; sex: string } ✅
深入理解
另一种解法:交叉类型
另一个常见解法是用 Omit + 交叉类型:
type Merge<F, S> = Omit<F, keyof S> & S原理:
- 从
F中删除所有在S中存在的键 - 将剩余部分与
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 是惯用写法。在类型体操中,映射类型方案更能体现原理。
核心要点
keyof F | keyof S在映射类型中收集两个类型的全部键——这是实现合并类工具的核心构件- 嵌套三元表达式在映射类型的值位置实现优先级解析(
S优先于F) Omit<F, keyof S> & S是惯用简写,但结果是交叉类型而非扁平对象类型- 条件类型中不可达的
never分支在语法上是必须的,用来封闭所有可能的分支
