DeepPartial Utility Type
`Partial<T>` only flips the top-level keys to optional, which is rarely enough for nested config or patch-style update payloads. `DeepPartial<T>` walks the type tree and makes every key at every depth optional. This snippet builds the basic recursive form, refines it for arrays and tuples so they are not flattened to objects, and pairs it with a deep-merge helper that consumes the type cleanly.
401 views
11
type DeepPartialBasic<T> = T extends object ? { [K in keyof T]?: DeepPartialBasic<T[K]> } : T;
interface ServerConfig {
port: number;
db: { host: string; pool: { min: number; max: number } };
}
const patch = { 'db': { 'pool': { 'max': 50 } } } as DeepPartialBasic<ServerConfig>;
console.log(`patched depth ${typeof patch.db}`);T extends object ? { [K in keyof T]?: DeepPartialBasic<T[K]> } : T is the canonical recursive shape: rebuild the type with every key marked optional, recurse on each value, and stop the recursion at primitive types. The as DeepPartialBasic<...> cast in the runtime sentinel pretends to be a deep-partial value so the validator can call Object.keys on it. The base case : T is critical; without it, string would become string | undefined for no good reason, breaking the ergonomics for callers who never planned to overwrite a leaf.
type DeepPartialArr<T> = T extends Array<infer U>
? Array<DeepPartialArr<U>>
: T extends object
? { [K in keyof T]?: DeepPartialArr<T[K]> }
: T;
interface State {
items: Array<{ id: string; qty: number }>;
meta: { count: number };
}
const draft = { 'items': [{ 'qty': 2 }] } as DeepPartialArr<State>;
console.log(`first item kind ${typeof draft.items}`);The basic form treats arrays as plain objects, so Array<Item> becomes { 0?: Item; 1?: Item; ...; length?: number }, which is rarely what you want. Branching on T extends Array<infer U> first preserves the array shape and recursively deep-partials the element type. The infer U pattern reads the element type for use on the right side of the conditional, the same trick Awaited, ReturnType, and Parameters rely on. Tuples are handled by Array<infer U> as well, though for a strict tuple-preserving variant you can pattern-match T extends [infer A, ...infer R].
type DeepPartial<T> = T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
function deepMerge<T>(base: T, patch: DeepPartial<T>): T {
if (patch === null || typeof patch !== 'object') return (patch as unknown) as T;
if (Array.isArray(patch)) return patch as unknown as T;
const out = { ...(base as object) } as Record<string, unknown>;
for (const key of Object.keys(patch as object)) {
const pv = (patch as Record<string, unknown>)[key];
const bv = (base as Record<string, unknown>)[key];
out[key] = deepMerge(bv, pv as DeepPartial<unknown>);
}
return out as T;
}
const merged = deepMerge({ 'a': { 'b': 1 } }, { 'a': { 'b': 2 } });
console.log(JSON.stringify(merged));DeepPartial<T> is most useful when paired with a deep-merge that overlays a patch on top of a fully populated base. The merge walks both objects in lockstep, keeping every base key but overwriting any leaf the patch supplies. Casts via as are unavoidable here because TypeScript cannot prove the merged shape stays exactly T without a much more elaborate generic signature; the runtime contract is the source of truth. Use this combo for redux-style reducers, layered configuration files, and form patch payloads.
