Recreate Object.assign at the type level: merge an array of sources into a target, later sources winning. Intersections collapse conflicts to never, so they're out.
Assign<T, U> copies the properties of every object in the source array U onto the target object T. When a key exists in both, the source property replaces the target property, the same rule the runtime Object.assign follows. Solving it takes two workhorse patterns of advanced TypeScript: merging two object types with a mapped type over a union of keys, and recursing through a tuple with infer First / infer Rest.
type Target = {
a: 'a'
}
type Origin1 = {
b: 'b'
}
// type Result = Assign<Target, [Origin1]>
type Result = {
a: 'a'
b: 'b'
}type Target = {
a: 'a'
d: {
hi: 'hi'
}
}
type Origin1 = {
a: 'a1',
b: 'b'
}
type Origin2 = {
b: 'b2',
c: 'c'
}
// type Answer = Assign<Target, [Origin1, Origin2]>
type Answer = {
a: 'a1',
b: 'b2',
c: 'c'
d: {
hi: 'hi'
}
}You have a target object and a source array of objects. You need to copy property from source to target, if it has the same property as the source, you should always keep the source property, and drop the target property. (Inspired by the Object.assign API)
type Target = {
a: 'a'
}
type Origin1 = {
b: 'b'
}
// type Result = Assign<Target, [Origin1]>
type Result = {
a: 'a'
b: 'b'
}type Target = {
a: 'a'
d: {
hi: 'hi'
}
}
type Origin1 = {
a: 'a1',
b: 'b'
}
type Origin2 = {
b: 'b2',
c: 'c'
}
type Answer = {
a: 'a1',
b: 'b2',
c: 'c'
d: {
hi: 'hi'
}
}View on GitHub: https://tsch.js.org/9160
Change the following code to make the test cases pass (no type check errors).
👋 Lifetime-License is leaving on August 10, 2026
Get it now for $29
One-time payment. Lifetime access to all pro challenges.
The solution splits into two types:
type Merge<T, U> = {
[K in keyof T | keyof U]: K extends keyof U
? U[K]
: K extends keyof T
? T[K]
: never
}
type Assign<T extends Record<string, unknown>, U> = U extends [
infer First,
...infer Rest,
]
? Assign<
First extends Record<string, unknown> ? Merge<T, First> : T,
Rest
>
: TTwo sub-problems hide in the statement: how do you merge two objects, and how do you repeat that for every element of a tuple. Each gets its own type.
Your first instinct might be an intersection: T & U. But intersections don't overwrite. { a: 1 } & { a: 2 } collapses to { a: never }, because no value can be both 1 and 2 at once. To get "source wins" semantics you have to build a fresh object type instead:
type Merge<T, U> = {
[K in keyof T | keyof U]: K extends keyof U
? U[K]
: K extends keyof T
? T[K]
: never
}keyof T | keyof U is the union of every key from both objects, so the mapped type iterates over all of them.K, the conditional asks the source first: if K extends keyof U, take U[K]. Only keys the source doesn't have fall through to T[K].never branch is unreachable (every K came from one of the two objects), but a conditional type's false branch is required syntax, so we have to write something there.A quick intermediate check:
type Step = Merge<{ a: 1; b: ['b'] }, { a: 2; c: 'c1' }>
// { a: 2; b: ['b']; c: 'c1' } : 'a' taken from the source, 'b' kept, 'c' addedNote the order of the two checks is what encodes the rule "always keep the source property, drop the target property".
Assign itself never merges anything. It drives the recursion over the tuple U:
[object Object]Tuple inference with a rest element splits U into its head and tail: for [Origin1, Origin2, Origin3], First is Origin1 and Rest is [Origin2, Origin3]. Each step merges First into the accumulated target and recurses on Rest. Here T doubles as an accumulator: it starts as the original target and grows one merge per step:
Assign<{}, [A, B, C]>
// → Assign<Merge<{}, A>, [B, C]>
// → Assign<Merge<Merge<{}, A>, B>, [C]>
// → Assign<Merge<Merge<Merge<{}, A>, B>, C>, []>
// → the fully merged objectWhen the tuple is empty, [] no longer matches [infer First, ...infer Rest], so the accumulated T is returned. Because later sources are merged later, they naturally overwrite earlier ones. That's how case 3 in the tests ends with a: 3 and c: 'c2' from Origin2 rather than the values from Origin1.
The last test case is sneaky: Assign<Case4Target, ['', 0]> must return the target untouched. The runtime Object.assign ignores primitives in the source list, and the type does the same with a guard inside the recursive call:
[object Object]If the current element isn't an object type, no merge happens and the recursion moves on. Without this guard, Merge would try to iterate keyof '' (which includes all the string methods) and produce garbage.
{}) works because keyof {} contributes nothing to the key union; the result is built entirely from the sources.a: [1, 2, 3] is replaced wholesale by a: { a1: 'a1' }. Merge never looks at the old type, so any replacement is fine.'', 0) are ignored, leaving the target unchanged.The head/tail recursion with an accumulator you used here is the standard template for any "reduce a tuple" problem in the type system. You'll reach for it again.
This challenge is originally from here.