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.
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).
/* _____________ 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: strGet access to all 200+ challenges, including every medium, hard, and extreme one.
Monthly subscription.
Cancel anytime + 30-day money-back guarantee.
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>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.
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:
{ [P in H]: T[P] } is the subset that contains only HSubsets<T, Exclude<K, H>> is every subset that does not mention HWithKey<T, H, Subsets<T, Exclude<K, H>>> is every one of those with H addedThat 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.
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] }
: neverS 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.
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.
{ a: string }, the recursive calls hit the empty-union base case and contribute never, so the union is a single object.Equal is not bothered.Record<'a01' | ... | 'a17', 1> gives every key the same value type, and the subsets still stay distinct because their keys differ.This challenge is originally from here.
Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges