#37606•Hard

Defined Partial Record 2

Build the union of every non-empty subset of an object's keys, with each variant flat and distinct, and keep it cheap enough to survive a 17-key record.

Partial<T> happily accepts an empty object. This one refuses.

DefinedPartial<T> produces the union of every non-empty subset of T's properties, each key in a subset still required. It is the shape you want for a patch payload or an update argument that must not be a no-op: at least one field present, and whatever is present is fully typed.

type A = DefinedPartial<{ a: string; b: number }>
// { a: string } | { b: number } | { a: string; b: number }

The "2" in the name is about scale. The last test feeds in a record with 17 keys, so the answer has 2^17 - 1 = 131071 members. A construction that looks fine on three keys can exhaust the compiler's heap on seventeen, which makes the cost per subset the real subject of this challenge.

Challenge Instructions: Defined Partial Record 2

Hard

A more difficult version of the challenge Defined Partial Record (34857, medium), the solution must work with objects that have up to 17 keys (then we run into the limit on the size of unions, this limit is less than 2^18-1 in TS 6)

View on GitHub: https://tsch.js.org/37606

Change the following code to make the test cases pass (no type check errors).

ChallengeSolution
/* _____________ Your Code Here _____________ */

type DefinedPartial<T> = any

/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'

type A1 = { a: string };
type E1 = { a: string };
type D1 = DefinedPartial<A1>;
type C1 = Expect<Equal<D1, E1>>;

type A2 = { a: string; b: number };
type E2 = { a: string } | { b: number } | { a: string, b: number };
type D2 = DefinedPartial<A2>;
type C2 = Expect<Equal<D2, E2>>;

type A3 = { a: string; b: number; c: boolean };
type E3 = { a: string } | { b: number } | { c: boolean }
  | { a: string, b: number } | { a: str

Pro Challenge

Get access to all 200+ challenges, including every medium, hard, and extreme one.

Monthly subscription.
Cancel anytime + 30-day money-back guarantee.

Detailed Explanation

The solution in full:

type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void
  ? I
  : never
 
type LastKey<U> = UnionToIntersection<U extends unknown ? () => U : never> extends () => infer L
  ? L
  : never
 
type WithKey<T, K extends PropertyKey, S> = S extends object
  ? { [P in (keyof S | K) & keyof T]: T[P] }
  : never
 
type Subsets<T, K extends keyof T> = [K] extends [never]
  ? never
  : LastKey<K> extends infer H extends keyof T
    ? { [P in H]: T[P] } | Subsets<T, Exclude<K, H>> | WithKey<T, H, Subsets<T, Exclude<K, H>>>
    : never
 
type DefinedPartial<T> = Subsets<T, keyof T>

Peeling one key off a union

To recurse over keys you need a way to grab a single one. LastKey does that by abusing inference.

U extends unknown ? () => U : never is a distributive conditional type: because U sits naked on the left of extends, TypeScript applies the branch to each member separately and unions the results. For 'a' | 'b' you get (() => 'a') | (() => 'b').

UnionToIntersection then turns that union into an intersection, by putting each member in a function parameter position and letting inference collect the candidates:

// UnionToIntersection<(() => 'a') | (() => 'b')>
//   = (() => 'a') & (() => 'b')

An intersection of two call signatures is an overload set, and when you infer a return type from an overloaded signature TypeScript resolves against the last overload:

// LastKey<'a' | 'b'>       = 'b'
// LastKey<'a' | 'b' | 'c'> = 'c'

Which key comes out last is an implementation detail, and it does not matter here. All the recursion needs is some key, and the guarantee that Exclude<K, H> is strictly smaller.

Growing the subsets

Subsets<T, K> returns every non-empty subset of K, already shaped as an object type. With H peeled off, the subsets split into exactly three groups:

That gives f(n) = 2 * f(n - 1) + 1, which is 2^n - 1. The recursion terminates because Exclude<K, H> drops a key on every step and bottoms out at [K] extends [never]. The brackets there matter: they switch distribution off, so the test asks "is the whole key union empty" rather than running once per key.

Keeping each variant flat

WithKey is where the subsets actually get built:

type WithKey<T, K extends PropertyKey, S> = S extends object
  ? { [P in (keyof S | K) & keyof T]: T[P] }
  : never

S arrives as a union of object types, and it is naked on the left of extends, so the mapped type is applied to each member on its own. Without that distribution you would collapse the whole union into one object carrying every key.

keyof S reads the key set back out of a subset that was already built, so there is no need for a separate carrier such as a tuple of keys. Re-running the mapped type over keyof S | K also keeps the result a single flat object. Reaching for S & { [P in K]: T[P] } instead is tempting and wrong: an intersection is not identical to the flattened object, and the tests compare with Equal, which asks for identity.

The & keyof T is only there to convince the compiler that P can index T.

Why 17 keys do not blow up

Mapped types are lazy. The 131071 members of the big case are created as unresolved mapped types, and nothing ever forces their properties, because the test only declares the type. Storing the key set inside the object rather than alongside it keeps that count at one type per subset, which is what makes the difference between a few seconds of tsc and running out of memory.

Edge cases the tests cover

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges