Turn a union of [key, value] tuples into an object type, the type-level Object.fromEntries. One mapped type does it, once you know in can iterate any union.
ObjectFromEntries<T> is the type version of Object.fromEntries. Given a union where each member is a two-element tuple of a key and its value type, you produce the object type those entries describe. The core technique, mapping over a union and remapping keys with as, is the same one behind real-world types for Object.entries round-trips, form libraries, and API schema builders.
For example:
interface Model {
name: string;
age: number;
locations: string[] | null;
}
type ModelEntries = ['name', string] | ['age', number] | ['locations', string[] | null];
type result = ObjectFromEntries<ModelEntries> // expected to be ModelImplement the type version of Object.fromEntries
For example:
interface Model {
name: string;
age: number;
locations: string[] | null;
}
type ModelEntries = ['name', string] | ['age', number] | ['locations', string[] | null];
type result = ObjectFromEntries<ModelEntries> // expected to be ModelView on GitHub: https://tsch.js.org/2949
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 whole solution is one mapped type:
type ObjectFromEntries<T extends [PropertyKey, unknown]> = {
[E in T as E[0]]: E[1]
}Two lines. They lean on a mapped-type feature that surprises even experienced TypeScript developers: you can map over things that aren't keys at all.
A classic mapped type iterates over a union of keys: [K in 'a' | 'b']: .... But the in clause accepts any union, including a union of tuples. In [E in T ...], the type parameter E takes on each member of the union one at a time:
// For T = ModelEntries, E is successively:
// ['name', string]
// ['age', number]
// ['locations', string[] | null]There's a catch, though: a tuple like ['name', string] isn't a valid property key, so on its own [E in T] would be an error. That's where key remapping comes in.
asThe as clause in a mapped type lets you compute the actual key from whatever you're iterating over. E[0] is an indexed access type; it reads the type at position 0 of the tuple, i.e. the key:
// When E = ['name', string]:
// E[0] = 'name' β becomes the property name
// E[1] = string β becomes the property valueSo [E in T as E[0]]: E[1] reads as: "for each entry E in the union, create a property named E[0] whose type is E[1]". The three union members produce the three properties of Model, and the Equal test passes because mapped types compare structurally against interfaces.
The crucial insight is that E keeps the pairing intact. If you instead tried to extract all keys and all values separately (T[0] gives 'name' | 'age' | 'locations' and T[1] gives string | number | string[] | null), you'd lose the association and every property would end up with the full value union. Iterating entry-by-entry is what keeps name matched with string and age with number.
T extends [PropertyKey, unknown] promises the compiler two things: every union member is (at least) a two-element tuple, and its first element is usable as a property key (string | number | symbol). Without it, E[0] wouldn't be a legal index (the compiler can't prove position 0 exists) and the as E[0] clause wouldn't be a legal key. Constraints like this are how you make indexed access types safe inside generic code.
If you expand the mapped type by hand, you can watch the union distribute:
{
[E in ['name', string] | ['age', number] | ['locations', string[] | null] as E[0]]: E[1]
}
// step 1: E = ['name', string] β name: string
// step 2: E = ['age', number] β age: number
// step 3: E = ['locations', string[] | null] β locations: string[] | null
// result: { name: string; age: number; locations: string[] | null }Values that are themselves unions (like string[] | null) pass through untouched; E[1] just reads whatever type sits in the second slot, union or not.
Once you know that in can iterate over any union and as can derive the key, a whole class of "restructure this union into an object" problems becomes a one-liner.
This challenge is originally from here.