【问题标题】:Deep Merge generic function returns type neverDeep Merge 泛型函数从不返回类型
【发布时间】:2021-06-23 15:15:10
【问题描述】:

我创建了一个通用实用函数,它接受 2 个Objects 并将它们合并在一起。作为第二个参数传递的 Object 也将覆盖与作为第一个参数传递的 Object 重叠的键。

/**
 * 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

当我使用不同的键传入Objects 时,Typescript 似乎正确评估了返回类型,但是,使用具有重叠键的对象;我开始得到never{key: never} 的返回类型

据我了解,never 暗示“类型”永远不会出现,尽管我不明白 typescript 是从哪里得出这个结论的。该函数在 vanilla JS 中按预期工作,所以我在声明类型时显然犯了一些错误。

有人可以帮助我了解 never 的来源,以及如何避免它吗?谢谢!

【问题讨论】:

  • 如果你说T &amp; K,而T就是例如{ p: string },而K{ p: number },那么属性需要同时是字符串和数字,这根本不可能。 type X = string &amp; number;X 将是 never
  • 请将代码的相关部分也编辑到问题中,这样它就不仅仅是编码在 URL 中。命名约定注意:泛型类型参数K一般用于指代键类型。如果您有类型 TKK 不扩展 keyof 任何东西,那就令人困惑了。我建议将K 更改为U 之类的东西,对于T 的伴侣类型,这更像是conventional
  • @jcalz 谢谢你,我会编辑代码并将 K 替换为 U 并在我的评论中添加代码的相关部分
  • @ASDFGerte 这是有道理的,感谢您澄清!

标签: javascript typescript typescript-generics


【解决方案1】:

您应该将类​​型视为值的子集。可能的最小子集是代表空集的never。例如:

const giveCreditCardToMyWife: never = true

它会抛出一个错误,说不可分配(感谢上帝!?)

问题似乎在于您使用的是 TS 中可能的第二小的子集:包含单个值的类型。示例:

type MySingleType = 'A'
type MyNumber = 42

您在测试中将DestSource 定义为:

type Dest = {
  readonly key: "I am a Key"
}

type Source = {
  readonly key: "I should overwrite Dest key"
}

我不明白为什么,但也许你的意思是这样的:

interface Dest {
  key: string;
}

interface Source {
  key: string;
}

如您所见,类型推断系统现在得出了正确的结论:

【讨论】:

  • 我如此具体地提供文字类型的原因只是为了具体。本质上,作为即将成为库的一部分的实用函数,它将与包括文字类型在内的多种类型一起使用。所以我想确保 Typescript 能够推断出正确的类型,无论是否是文字,而不是仅仅返回 never
【解决方案2】:

我想出了以下解决我的问题的答案

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>
>;

现在 Typescript 可以正确推断 Merged 返回类型。

代码在这里:Playground

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-10-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-08
    相关资源
    最近更新 更多