I have a DeepMerge<> generic below that is not supporting deep optional merges.
type DeepMerge<T, U> = [T, U] extends [object, object] ?
{
[K in keyof (U & Pick<T, Exclude<keyof T, keyof U>>)]: (
K extends keyof U ? (
K extends keyof T ? DeepMerge<T[K], U[K]> : U[K]
) : (
K extends keyof T ? T[K] : never
)
)
} : U;
I have a Person with a deep optional address.
type Person = {
name: string,
age: number,
address: {
line1: string,
line2: string | null | number,
zip: string | number,
address?: {
line1: string,
line2: string | null | number,
zip: string | number,
},
},
};
I want to overwrite the deep address and add burger.
type PersonOverwrites = {
address: {
pizzaDelivery: boolean,
address?: {
burger: boolean,
},
},
};
Here is is complaining:
const person: DeepMerge<Person, PersonOverwrites> = {
name: 'Thomas',
age: 12,
address: {
line1: 'hi',
line2: 'hi',
zip: 'hi',
pizzaDelivery: true,
address: {
line1: 'hi',
line2: 'hi',
zip: 'hi',
burger: true,
},
},
};
It should require line1, line2, zip, burger.

Type '{ line1: string; line2: string; zip: string; burger: true; }' is not assignable to type '{ burger: boolean; }'. Object literal may only specify known properties, and 'line1' does not exist in type '{ burger: boolean; }'.
It should be supporting { line1: string; line2: string; zip: string; burger: true; } but instead the entire deep object is being overwritten to { burger: boolean; }.
Hmm, I think the problem here is that keyof ({a: string} | undefined) is showing up as never so as soon as the property is optional, DeepMerge can't recurse any farther down. There are probably other ways to fix this, but for a first shot, how about changing T and U to NonNullable versions of themselves, where NonNullable is defined in the standard library as
type NonNullable<T> = T extends null | undefined ? never : T;
Let's try it:
type DeepMerge<_T, _U, T= NonNullable<_T>, U= NonNullable<_U>> =
[T, U] extends [object, object] ?
{
[K in keyof (U & Pick<T, Exclude<keyof T, keyof U>>)]: (
K extends keyof U ? (
K extends keyof T ? DeepMerge<T[K], U[K]> : U[K]
) : (
K extends keyof T ? T[K] : never
)
)
} : U;
Does that work? That infers the type of person as
const person: {
address: {
pizzaDelivery: boolean;
address?: {
burger: boolean;
line1: string;
line2: string | number | null;
zip: string | number;
} | undefined;
line1: string;
line2: string | number | null;
zip: string | number;
};
name: string;
age: number;
}
EDIT: it's also possible that all you want is to distribute across all unions, like this:
type DeepMerge<T, U> = T extends any ? U extends any ?
[T, U] extends [object, object] ?
{
[K in keyof (U & Pick<T, Exclude<keyof T, keyof U>>)]: (
K extends keyof U ? (
K extends keyof T ? DeepMerge<T[K], U[K]> : U[K]
) : (
K extends keyof T ? T[K] : never
)
)
} : U : never : never;
Maybe that works better for you?
Hope that helps. Good luck!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With