Last active
February 6, 2024 02:40
-
-
Save minenwerfer/3e8c2078bc3c4bafec763b1b439d2aa4 to your computer and use it in GitHub Desktop.
Deep merge two objects in Typescript, including arrays
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This utility type merges two objects together while handling arrays. | |
* An intersection of objects (L & R) can behave oddly when dealing with arrays: | |
* | |
* Typescript doesn't complain about a type error in t.a, because never[] & 'something'[] => never | |
* declare const t: { a: readonly [] } & { a: readonly 'something'[] } | |
* t.a[0] === 'x' | |
* | |
* Likewise, the below is also valid, because readonly 'a'[] & readonly 'b'[] also results in never | |
* because both types don't overlap with each other and ts won't turn them into ('a' | 'b')[] like | |
* you could expect. | |
* declare const t: { a: readonly 'a'[] } & { a: readonly 'b'[] } | |
* t.a[0] === 'c' | |
* | |
* A IsString<T> utility type is used to replace Left with Right when Left is string[], because | |
* making a union of string and literal types would result in any string being accepted. | |
*/ | |
type IsString<T> = T extends string | |
? T extends `${T}${T}` | |
? true | |
: false | |
: false | |
type DeepMerge<L, R> = { | |
[P in keyof L | keyof R]: P extends keyof R | |
? P extends keyof L | |
? L[P] extends infer Left | |
? R[P] extends infer Right | |
? Right extends readonly (infer R0)[] | |
? Left extends readonly (infer L0)[] | |
? IsString<L0> extends true | |
? Right | |
: readonly (R0 | L0)[] | |
: Right | |
: Right extends Record<string, any> | |
? Left extends Record<string, any> | |
? Left extends (...args: any[]) => any | |
? Right | |
: DeepMerge<Left, Right> | |
: Right | |
: Right | |
: never | |
: never | |
: R[P] | |
: P extends keyof L | |
? L[P] | |
: never | |
} | |
type Object1 = { | |
name: 'joao' | |
pets: readonly ( | |
| 'dog' | |
| 'bird' | |
)[] | |
deep: { | |
colors: readonly ( | |
| 'red' | |
)[] | |
} | |
} | |
type Object2 = { | |
name: 'not-joao' | |
pets: readonly ( | |
| 'turtle' | |
)[] | |
deep: { | |
colors: readonly ( | |
| 'purple' | |
)[] | |
} | |
} | |
declare const merged: DeepMerge<Object1, Object2> | |
merged.name === 'not-joao' | |
// @ts-expect-error | |
merged.name === 'joao' | |
merged.pets.includes('dog') | |
merged.pets.includes('bird') | |
merged.pets.includes('turtle') | |
// @ts-expect-error | |
merged.pets.includes('cat') | |
merged.deep.colors.includes('red') | |
merged.deep.colors.includes('purple') | |
// @ts-expect-error | |
merged.deep.colors.includes('yellow') | |
export {} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment