AJP
AJP

Reputation: 28453

Type safe merge of object literals in typescript

I want to merge two typescript objects (using object spread):

var one = { a: 1 }
var two = { a: 2, b: 3 }
var m = {...one, ...two} // problem as property `a` is overwritten

I want to use the type system to ensure none of the properties in the second object overwrite any properties in the first. I am not sure why the following solution does not work:

type UniqueObject<T extends {[K in keyof U]?: any}, U> =
    {[K in keyof U]: T[K] extends U[K] ? never : U[K]}

var one = { a: 1 }
var two1 = { a: 2, b: 3 }
var two1_: UniqueObject<typeof one, typeof two1> = two1 // errors correctly
var two2 = { a: undefined, b: 1 }
var two2_: UniqueObject<typeof one, typeof two2> = two2 // passes incorrectly

Another version from a year ago which I thought worked at the time had undefined extends U[K] in the place of T[K] extends U[K]:

type UniqueObject<T extends {[K in keyof U]?: any}, U> =
    {[K in keyof U]: undefined extends T[K] ? U[K]: never}

Neither of these two work. I suspect it is because the undefined extends U[K] or T[K] extends U[K] are both false as the property K in T is optional. Not sure how or if it's possible to get around this.

Upvotes: 0

Views: 1618

Answers (3)

Michael P. Scott
Michael P. Scott

Reputation: 714

Try

type Merge<A, B> = { [K in keyof (A | B)]: K extends keyof B ? B[K] : A[K] };

This type allows you to specify any two objects, A and B.

From these, a mapped type whose keys are derived from available keys from either object is created. The keys come from keyof (A | B).

Each key is then mapped to the type of that key by looking up the appropriate type from the source. If the key comes from B, then the type is the type of that key from B. This is done with K extends keyof B ?. This part asks the question, "is K a key from B" ? To get the type of that key, K, use a property lookup B[K].

If the key is not from B, it must be from A, thus the ternary is completed:

K extends keyof B ? B[K] : A[K]

All of this is wrapped in an object notation { }, making this a mapped object type, whose keys are derived from two object and whose types map to the source types.

Upvotes: 0

AJP
AJP

Reputation: 28453

Taking @ford04's answer and expanding it for multiple optional values:

function safe_merge<
    O1,
    O2 extends { [K2 in keyof O2]: K2 extends keyof O1 ? never : O2[K2] },
    O3 extends { [K3 in keyof O3]: K3 extends keyof O1 ? never : (K3 extends keyof O2 ? never : O3[K3]) },
    O4 extends { [K4 in keyof O4]: K4 extends keyof O1 ? never : (K4 extends keyof O2 ? never : (K4 extends keyof O3 ? never : O4[K4])) },
    O5 extends { [K5 in keyof O5]: K5 extends keyof O1 ? never : (K5 extends keyof O2 ? never : (K5 extends keyof O3 ? never : ( K5 extends keyof O4 ? never : O5[K5]))) },
    O6 extends { [K6 in keyof O6]: K6 extends keyof O1 ? never : (K6 extends keyof O2 ? never : (K6 extends keyof O3 ? never : ( K6 extends keyof O4 ? never : (K6 extends keyof O5 ? never : O6[K6])))) },
    O7 extends { [K7 in keyof O7]: K7 extends keyof O1 ? never : (K7 extends keyof O2 ? never : (K7 extends keyof O3 ? never : ( K7 extends keyof O4 ? never : (K7 extends keyof O5 ? never : (K7 extends keyof O6 ? never : O7[K7]))))) },
    O8 extends { [K8 in keyof O8]: K8 extends keyof O1 ? never : (K8 extends keyof O2 ? never : (K8 extends keyof O3 ? never : ( K8 extends keyof O4 ? never : (K8 extends keyof O5 ? never : (K8 extends keyof O6 ? never : (K8 extends keyof O7 ? never : O8[K8])))))) },
    O9 extends { [K9 in keyof O9]: K9 extends keyof O1 ? never : (K9 extends keyof O2 ? never : (K9 extends keyof O3 ? never : ( K9 extends keyof O4 ? never : (K9 extends keyof O5 ? never : (K9 extends keyof O6 ? never : (K9 extends keyof O7 ? never : (K9 extends keyof O8 ? never : O9[K9]))))))) },
>(
    o1: O1,
    o2: O2 = ({} as any),
    o3: O3 = ({} as any),
    o4: O4 = ({} as any),
    o5: O5 = ({} as any),
    o6: O6 = ({} as any),
    o7: O7 = ({} as any),
    o8: O8 = ({} as any),
    o9: O9 = ({} as any),
): O1 & O2 & O3 & O4 & O5 & O6 & O7 & O8 & O9 {
    return { ...o1, ...o2, ...o3, ...o4, ...o5, ...o6, ...o7, ...o8, ...o9 }
}


const obj_1 = {1:1}
const obj_2 = {2:1}
const obj_3 = {3:1}
const obj_4 = {4:1}
const obj_5 = {5:1}
const obj_6 = {6:1}
const obj_7 = {7:1}
const obj_8 = {8:1}
const obj_9 = {9:1}


// should not error
safe_merge(obj_1)
safe_merge(obj_1, obj_2)
safe_merge(obj_1, obj_2, obj_3)
safe_merge(obj_1, obj_2, obj_3, obj_4)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9)


// declare objects with keys conflicting with existing objects
const obj_2_1 = {2:1, 1: 1}
const obj_2_1b = {2:1, 1: undefined}

const obj_3_1 = {3:1, 1:1}
const obj_3_2 = {3:1, 2:1}

const obj_4_1 = {4:1, 1:1}
const obj_4_2 = {4:1, 2:1}
const obj_4_3 = {4:1, 3:1}

const obj_5_1 = {5:1, 1:1}
const obj_5_2 = {5:1, 2:1}
const obj_5_3 = {5:1, 3:1}
const obj_5_4 = {5:1, 4:1}

const obj_6_1 = {6:1, 1:1}
const obj_6_2 = {6:1, 2:1}
const obj_6_3 = {6:1, 3:1}
const obj_6_4 = {6:1, 4:1}
const obj_6_5 = {6:1, 5:1}

const obj_7_1 = {7:1, 1:1}
const obj_7_2 = {7:1, 2:1}
const obj_7_3 = {7:1, 3:1}
const obj_7_4 = {7:1, 4:1}
const obj_7_5 = {7:1, 5:1}
const obj_7_6 = {7:1, 6:1}

const obj_8_1 = {8:1, 1:1}
const obj_8_2 = {8:1, 2:1}
const obj_8_3 = {8:1, 3:1}
const obj_8_4 = {8:1, 4:1}
const obj_8_5 = {8:1, 5:1}
const obj_8_6 = {8:1, 6:1}
const obj_8_7 = {8:1, 7:1}

const obj_9_1 = {9:1, 1:1}
const obj_9_2 = {9:1, 2:1}
const obj_9_3 = {9:1, 3:1}
const obj_9_4 = {9:1, 4:1}
const obj_9_5 = {9:1, 5:1}
const obj_9_6 = {9:1, 6:1}
const obj_9_7 = {9:1, 7:1}
const obj_9_8 = {9:1, 8:1}


// should error
safe_merge(obj_1, obj_2_1)
safe_merge(obj_1, obj_2_1b)

safe_merge(obj_1, obj_2, obj_3_1)
safe_merge(obj_1, obj_2, obj_3_2)

safe_merge(obj_1, obj_2, obj_3, obj_4_1)
safe_merge(obj_1, obj_2, obj_3, obj_4_2)
safe_merge(obj_1, obj_2, obj_3, obj_4_3)

safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5_1)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5_2)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5_3)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5_4)

safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6_1)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6_2)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6_3)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6_4)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6_5)

safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7_1)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7_2)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7_3)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7_4)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7_5)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7_6)

safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8_1)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8_2)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8_3)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8_4)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8_5)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8_6)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8_7)

safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9_1)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9_2)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9_3)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9_4)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9_5)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9_6)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9_7)
safe_merge(obj_1, obj_2, obj_3, obj_4, obj_5, obj_6, obj_7, obj_8, obj_9_8)

Upvotes: 0

ford04
ford04

Reputation: 74500

Both your versions are more or less equivalent - only the true/false branches in the conditional type are switched up.

The constraint T extends {[K in keyof U]?: any} is a bit problematic: when you remove a in two, the error Type '{ a: number; }' has no properties in common with type '{ b?: any; } is triggered, which actually should be the success case.

Be also aware, that the resulting type merge doesn't contain the merged type definition from both types. We can change the declaration up:

type UniqueObject<T, U> =
    T & { [K in keyof U]: K extends keyof T ? never : U[K] }

Now, the compiler correctly errors with a duplicate a property:

var one = { a: 1 }
var two = { a: 2, b: 3 }
//                                                        v a becomes never here
type Merge = UniqueObject<typeof one, typeof two> // { a: never; b: number; }
const res: Merge = { ...one, ...two } // errors now, cannot assign number to never

In the following I have simplified the type a bit and packed everything in a compact helper function to control types + spread operator:

function mergeUnique<T extends object, U extends object & { [K in keyof U]: K extends keyof T ? never : U[K] }>(o1: T, o2: U) {
    return { ...o1, ...o2 }
}

const res21 = mergeUnique({ a: 1 }, { b: 3 })
const res22 = mergeUnique({ a: 1 }, { a: 2, b: 3 }) // error
const res23 = mergeUnique({ a: 1, c: 5 }, { b: 3 })
const res24 = mergeUnique({ a: 1}, { a: undefined }) // error

Code sample

Upvotes: 1

Related Questions