#DeepPartial
The challenge: write DeepPartial<T> — like Partial<T>, but every nested object's properties become optional too, all the way down. My notes say this one "broke me", and that a custom deep partial "will break you 9000 times". It's worth seeing exactly why the obvious version breaks, because the fix teaches the general recipe for every deep type.
The idea: Partial is a mapped type that adds ? to the top level only. To go deeper, the value type of each property has to be DeepPartial of the original value type — a recursive type. The hard part isn't the recursion; it's deciding what to do with things that aren't plain objects: primitives, arrays, functions, and built-ins like Date.
The version everyone writes first:
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type DeepPartialNaive<T> = { [K in keyof T]?: DeepPartialNaive<T[K]> };
interface Settings {
theme: { color: string; fontSize: number };
tags: string[];
onSave: (s: string) => void;
}
type N = DeepPartialNaive<Settings>;
type T1 = Expect<Equal<N["theme"], { color?: string; fontSize?: number } | undefined>>; // good
type T2 = Expect<Equal<N["tags"], (string | undefined)[] | undefined>>; // holes allowed
type T3 = Expect<Equal<N["onSave"], {} | undefined>>; // not a function any more!
const oops: N = { onSave: {}, tags: [undefined] }; // compilesDeepPartialNaive<Settings>loops over"theme" | "tags" | "onSave", adds?to each, and recurses on each value type. (| undefinedappears in the assertions because reading an optional property givesX | undefined.)theme:DeepPartialNaive<{ color: string; fontSize: number }>recurses once more and makes both optional. ThenDeepPartialNaive<string>hits a primitive, and a homomorphic mapped type returns primitives unchanged — so recursion stops by itself. This part works.tags:DeepPartialNaive<string[]>is a homomorphic mapped type applied to an array, so it maps the elements — and the?makes each element optional, giving(string | undefined)[]. Now[undefined]is a valid tag list. Not what anyone meant.onSave: a function type has no properties forkeyofto find, so the mapped type produces{}— an empty object type, which accepts almost anything ({},5,"x"). The callback type is simply gone, which is whyonSave: {}compiles.- So the naive version is right for plain objects and wrong for the two most common non-plain values. The fix is to branch on what kind of type we have before mapping.
DeepPartial solutionThe solution — check functions first, then arrays, then objects, then fall through for primitives:
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type DeepPartial<T> =
T extends (...args: any[]) => unknown ? T
: T extends readonly unknown[] ? { [K in keyof T]: DeepPartial<T[K]> }
: T extends object ? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface Settings {
theme: { color: string; fontSize: number };
tags: string[];
onSave: (s: string) => void;
servers: { host: string; port: number }[];
}
type T1 = Expect<Equal<DeepPartial<Settings>, {
theme?: { color?: string; fontSize?: number };
tags?: string[];
onSave?: (s: string) => void;
servers?: { host?: string; port?: number }[];
}>>;
type T2 = Expect<Equal<DeepPartial<[{ a: 1 }, string]>, [{ a?: 1 }, string]>>; // tuples keep their shape
type T3 = Expect<Equal<DeepPartial<string | null>, string | null>>;
const patch: DeepPartial<Settings> = { theme: { color: "red" }, servers: [{ port: 8080 }] };
const broken: DeepPartial<Settings> = { onSave: {} }; // ❌ TS2322: Type '{}' is not assignable to type '(s: string) => void'.- Top level,
T = Settings: not a function, not an array, but it is anobject, so the third branch maps every key with?and recurses on each value. Now each property is handled on its own. theme→DeepPartial<{ color: string; fontSize: number }>→ object branch again →{ color?: …; fontSize?: … }. ThenDeepPartial<string>: not a function, not an array, not an object → last branch returnsstring. That's the base case that ends the recursion.tags→DeepPartial<string[]>hits the array branch. It maps the array with a homomorphic mapped type but without?, so the result is still an array (string[]) with each element passed throughDeepPartial(stringstaysstring). Noundefinedholes.onSavehits the first branch and is returned as is — the function type survives, soonSave: {}now fails with "TS2322: Type '{}' is not assignable to type '(s: string) => void'." The function check must come before theobjectcheck, because functions are objects too.servers→ array branch → each element{ host: string; port: number }→ object branch →{ host?: string; port?: number }. So[{ port: 8080 }]is valid: the array is required to be an array, but each server can be partial. Because the array branch maps overkeyof T, tuples keep their exact shape too (T2).Tis a naked type parameter in everyextends, so the whole thing distributes over unions:DeepPartial<Address | null>handlesAddressandnullseparately —T3showsstring | nullpassing through untouched.
DeepPartial is usefulWhere you'd use it: PATCH-style update payloads for nested resources, test fixtures that override a few nested fields of a big default object, and config merging (merge(defaults, userConfig: DeepPartial<Config>)).
Date and Map in deep mapping