Skip to content

Instantly share code, notes, and snippets.

@minenwerfer
Last active February 6, 2024 02:40
Show Gist options
  • Save minenwerfer/3e8c2078bc3c4bafec763b1b439d2aa4 to your computer and use it in GitHub Desktop.
Save minenwerfer/3e8c2078bc3c4bafec763b1b439d2aa4 to your computer and use it in GitHub Desktop.
Deep merge two objects in Typescript, including arrays
/**
* 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