#2949β€’Hard

ObjectFromEntries

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 Model

Challenge Instructions: ObjectFromEntries

Hard

Implement 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 Model

View 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.

Loading...

Detailed Explanation

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.

Mapping over a union of tuples

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.

Key remapping with as

The 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 value

So [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.

Why the constraint matters

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.

The distribution, spelled out

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.

Share this challenge

Learn the Concepts