简体   繁体   中英

Deep Merge generic function returns type never

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




Playground

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: vscode 屏幕截图

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM