I have created a generic utility function that accepts 2 Objects
and merges them together. The Object
passed as the second argument will also overwrite the keys that overlap the Object
passed as the first argument.
/**
* Deeply merges two objects together with the source object overwriting the matching keys in the destination object.
*
* @param destination The object into where the source will be merged.
* @param source The object to merge into destination. (overwrites keys in destination).
* @returns A deep copy of the merged source and destination objects.
*/
export function deepMerge<T extends Object, K extends Object>(
destination: T,
source: K
): T & K {
const OUTPUT = deepCopy(destination) as T & K; //deepCopy<T extends Object>(obj: T): T Deep copies the object and returns it.
const keys = Object.getOwnPropertyNames(source) as Array<keyof K>;
for (const name of keys) {
const value = source[name];
if (
getCorrectType(value) === "object" &&
getCorrectType(OUTPUT[name]) === "object"
) {
OUTPUT[name] = deepMerge(OUTPUT[name], value);
} else {
OUTPUT[name] = value as (T & K)[keyof K];
}
}
return OUTPUT;
}
type Dest = {
readonly key: "I am a Key"
}
type Source = {
readonly key: "I should overwrite Dest key"
}
const dest1 = {
key: "Just a key"
}
const source1 = {
key2: "another key"
}
const dest2 = {
key: "Just another key"
}
const source2 ={
key: "Same key as dest1 so I will overwrite"
}
const dest3: Dest = {
key:"I am a Key"
}
const source3: Source = {
key: "I should overwrite Dest key"
}
const dest4 = {
key: 'I am a string'
}
const source4 = {
key: 90 //type number should overwrite dest4 type string
}
//Expect merged object of dest1 and source1
const res1 = deepMerge(dest1,source1)
//res1 = {key: string, key2: string} -> PASS
//Expect merged object of dest2 and source2 with only one key as the source2 key will overwrite dest2 key
const res2 = deepMerge(dest2, source2);
//res2 = {key: string} -> PASS
//Expect merged object of dest3 and source3 with source3 string literal overwriting dest3 string literal
const res3 = deepMerge(dest3, source3)
//res3 = never -> FAIL
//Expect merged object of dest4 and source4 with key of type number overwriting key of type string
const res4 = deepMerge(dest4, source4)
//res4 = {key: never} -> FAIL
Typescript seems to correctly evaluate the return type when I pass in Object
s with differing keys, however, with Objects that have overlapping keys; I start to get return types of never
or {key: never}
From what I understand, never
implies that the "type" would never occur, although I don't understand where typescript is drawing that conclusion from. The function works as expected in vanilla JS so I have obviously made some mistakes in declaring the types.
Can someone help me understand where never is coming from, and how it can be avoided? Thanks!
You should think of Types as a subset of values. The smallest possible subset is the never
that represents an empty set. For example:
const giveCreditCardToMyWife: never = true
It will cast an error saying is not assignable (thanks god! )
It seems that the problem is that you're using the second smallest subset possible in TS: the types that contains a single value. example:
type MySingleType = 'A'
type MyNumber = 42
You're defining Dest
and Source
in your tests as:
type Dest = {
readonly key: "I am a Key"
}
type Source = {
readonly key: "I should overwrite Dest key"
}
i didn't get why, but maybe you meant something like this:
interface Dest {
key: string;
}
interface Source {
key: string;
}
As you can see the type inferring system now comes to right conclusion:
I came up with the following answer which addresses my problems
export function deepMerge<D extends Object, S extends Object>(
destination: D,
source: S
): Spread<D, S> {
const OUTPUT: Record<PropertyKey, any> = deepCopy(destination);
const keys = Object.getOwnPropertyNames(source) as Array<keyof S>;
for (const name of keys) {
const value = source[name];
if (
getCorrectType(value) === "object" &&
getCorrectType(OUTPUT[name]) === "object"
) {
OUTPUT[name] = deepMerge(OUTPUT[name], value);
} else {
OUTPUT[name] = value as (D & S)[keyof S];
}
}
return OUTPUT as Spread<D, S>;
}
/**
* From T return the keys of those types that are assignable to undefined.
*/
type GetOptionalKeys<T> = {
[K in keyof T]: T[K] extends undefined ? K : never;
}[keyof T];
type MappedObject<T> = { [K in keyof T]: T[K] };
type MergeObject<D, S, K extends keyof D & keyof S> = {
[Key in K]: D[Key] | Exclude<S[Key], undefined>;
};
export type Spread<D, S> = MappedObject<
// Pick Properties in D that don't exist in S.
Pick<D, Exclude<keyof D, keyof S>> &
// Pick Properties in S with types that aren't undefined
Pick<S, Exclude<keyof S, GetOptionalKeys<S>>> &
//Pick Properties in S, with types that include undefined, that don't exist in L
Pick<S, Exclude<GetOptionalKeys<S>, keyof D>> &
// Merge D properties with S, with types that include undefined, that exist in D
MergeObject<D, S, GetOptionalKeys<S> & keyof D>
>;
Now the Typescript correctly infers the Merged return type.
Code Here: Playground
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.