Annotated solution
Published September 19, 2026The solution
type ZipObject< Keys extends readonly PropertyKey[], Values extends readonly unknown[] > = Prettify< Keys extends readonly [infer K extends PropertyKey, ...infer KR extends PropertyKey[]] ? Values extends readonly [infer V, ...infer VR] ? { [P in K]: V } & ZipObject<KR, VR> : {} : {} >
The common wrong answer
type ZipObject<Keys extends readonly PropertyKey[], Values extends readonly unknown[]> = Keys extends readonly [infer K extends PropertyKey, ...infer KR extends PropertyKey[]] ? Values extends readonly [infer V, ...infer VR] ? { [P in K]: V } & ZipObject<KR, VR> : {} : {}
The properties are correct, but the result remains an intersection of one-property objects. Assignability accepts it; this checker asks for exact identity, so the final mapped pass is part of the answer.
Line by line
{ [P in K]: V } & ZipObject<KR, VR>Each pass consumes one position from both tuples and intersects that property with the recursively built remainder.
Prettify<Mapping over the accumulated keys materialises one ordinary object type, which is the shape the checks name.
Takeaway
Recursive object builders often accumulate intersections. A final identity mapping is not cosmetic when exact equality matters.

