TypeScriptadvanced
Building DeepPartial<T> from scratch
Published December 22, 2026
export type DeepPartial<T> = T extends readonly unknown[]
? T
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
type Config = { server: { host: string; port: number }; features: { flags: string[] } };
export const mergeConfig = (base: Config, overrides: DeepPartial<Config>): Config => ({
server: { ...base.server, ...overrides.server },
features: { ...base.features, ...overrides.features },
});What
DeepPartial<T> recursively makes every property of T optional, at every level of nesting — not just the top level, the way the built-in Partial<T> does. mergeConfig uses it to accept a partial, nested patch and merge it over a full Config.
Why it matters
Partial<Config> only makes server and features themselves optional — if you provide server, you're back to needing every one of its fields, because Partial doesn't recurse into nested object properties. That's the wrong shape for a config patch, where you want to override server.port alone without also restating server.host. DeepPartial fixes that by applying the same "make it optional" transform recursively to every nested object type it finds.
How it works
- The
T extends readonly unknown[] ? T : ...branch is checked first and left untouched — recursing into array element types here would produceArray<DeepPartial<Element>>, which is rarely what you want for a patch, since you don't usually want to merge two arrays element-by-element. - The
T extends objectbranch covers plain nested objects likeserverandfeatures:{ [K in keyof T]?: DeepPartial<T[K]> }makes each property optional and recurses into it. - Anything that's neither an array nor an object (
string,number,boolean, ...) hits the final: Tbranch unchanged — that's the recursion's base case. mergeConfigspreadsbase's nested objects first, then spreadsoverrides' matching nested object over them, so provided fields win and omitted ones fall back tobase.
Gotchas
T extends objectis broader than "plain object" — it also matchesDate,Map, class instances, and functions. RunningDeepPartialover one of those makes its methods optional properties too, which type-checks but isn't meaningful. This implementation is fine for plain config-shaped data; a production-grade version usually special-casesDateand a few other built-ins to pass through unchanged, the same way arrays are handled here.- Because
DeepPartialdoesn't validate anything at runtime,mergeConfig's spread-based merge is doing the actual work — the type only describes what shape is allowed in, it doesn't perform the merge for you.
Related
Partial<T>— the shallow, built-in version this generalizes; reach for it when nesting isn't a concern.Required<T>— the inverse transform, making optional properties mandatory; the same recursive trick applies if you need aDeepRequired<T>.