Merge
Merge
Challenge
Merge two types into a new type. Keys of the second type override keys of the first type.
type foo = {
name: string
age: string
}
type coo = {
age: number
sex: string
}
type Result = Merge<foo, coo>
// expected: { name: string; age: number; sex: string }Solution
type Merge<F, S> = {
[K in keyof F | keyof S]: K extends keyof S ? S[K] : K extends keyof F ? F[K] : never
}The key insight: iterate over the union of all keys from both types, then for each key decide which type "wins" — with S (the second type) taking priority.
Breaking it down
Step 1 — Collect all keys: keyof F | keyof S
The mapped type iterates over every key that appears in either F or S. This ensures no properties are dropped.
Step 2 — Priority resolution via nested conditional
For each key K:
- If
Kis a key ofS→ useS[K](second type wins) - Else if
Kis a key ofF→ useF[K](first type's original value) - Else →
never(unreachable in practice, sinceKmust be from one of the two)
With our example:
name: only inF→stringage: in bothFandS, butSwins →numbersex: only inS→string
Result: { name: string; age: number; sex: string } ✅
Deep Dive
Alternative: Intersection approach
Another common solution uses Omit + intersection:
type Merge<F, S> = Omit<F, keyof S> & SThis works by:
- Stripping from
Fany keys that exist inS - Intersecting what's left with
S
It's concise, but the result type is an intersection (Omit<...> & S), not a plain object literal. TypeScript often displays this as a merged object in IDE tooltips, but it's technically a different shape. The mapped-type approach produces a clean, flat object type that's more explicit.
You can "flatten" the intersection with a mapped type trick:
type Merge<F, S> = {
[K in keyof (Omit<F, keyof S> & S)]: (Omit<F, keyof S> & S)[K]
}But at that point the direct mapped-type solution is cleaner.
Why keyof F | keyof S works in a mapped type
TypeScript allows union types in keyof position within mapped types:
type Keys = keyof { a: 1 } | keyof { b: 2 } // 'a' | 'b'
type M = {
[K in 'a' | 'b']: ... // iterates over both
}This is the foundation of many "merge two objects" patterns in the TypeScript type system.
The never branch
K extends keyof S ? S[K] : K extends keyof F ? F[K] : neverThe final never is unreachable in practice. Because K is constrained to keyof F | keyof S, it must satisfy at least one of the two conditions. It exists purely to satisfy TypeScript's type-checker (every branch of a conditional type must have a valid type).
Mapped type vs utility types
| Approach | Result shape | Readability |
|---|---|---|
| Direct mapped type | Flat object | ✅ Explicit |
Omit<F, keyof S> & S | Intersection | ⚠️ Implicit flattening |
{...(Omit<F,keyof S> & S)} | Flat object | 🔄 Two-step |
For production code, the Omit & S pattern is idiomatic TypeScript. For type challenges, the mapped-type form demonstrates the mechanics more clearly.
Key Takeaways
keyof F | keyof Sin a mapped type collects all keys from both types — the essential building block for merge-style utilities- Nested ternaries in mapped type values provide priority-based property resolution (
SbeatsF) Omit<F, keyof S> & Sis the idiomatic shorthand but produces an intersection type rather than a flat object- The
neverfallback in a conditional type is often unreachable but required by the type checker to close all branches
