Yuzhe's Blog

yuzhes

Merge

Merge

Challenge Link

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:

  1. If K is a key of S → use S[K] (second type wins)
  2. Else if K is a key of F → use F[K] (first type’s original value)
  3. Else → never (unreachable in practice, since K must be from one of the two)

With our example:

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> & S

This works by:

  1. Stripping from F any keys that exist in S
  2. 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] : never

The 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

ApproachResult shapeReadability
Direct mapped typeFlat object✅ Explicit
Omit<F, keyof S> & SIntersection⚠️ 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