【问题标题】:TypeScript problem inferring value type of restricted conditional key type推断受限条件键类型的值类型的 TypeScript 问题
【发布时间】:2020-10-14 08:47:21
【问题描述】:

假设我有以下实体模型:

interface Entity {
  id: string;
}

interface Member extends Entity {
  group: Group | string;
}

interface Group extends Entity {
  members: (Member | string)[];
}

我将分别检索GroupMember 实体的列表。在检索时,实体将包含对其他实体的字符串引用。例如,Member 实体将有一个 group 属性,其中包含该成员所属组的 ID。

现在,在检索后,我想通过将字符串引用替换为它们所引用的实体来对实体进行水合/膨胀。例如,以下调用应将 Member 实体中的 group 引用替换为实际的 Group 对象:

link(members, groups, 'group');

现在,我想将该调用中的第三个参数 ('group') 限制为仅允许可能引用 Group 实体的属性名称,因此我编写了以下类型定义:

type Ref<T, S> = { [K in keyof T]: S extends T[K] ? K : never }[keyof T];

这可以正常工作,例如type X = Ref&lt;Member, Group&gt; 将评估为 group

方法实现可能如下所示:

function link<T extends Entity, S extends Entity>(target: T[], source: S[], ref: Ref<T, S>) { 
  const lookup = new Map(source.map<[string, S]>(entity => [entity.id, entity]));

  target.forEach(entity => {
    const value = entity[ref];
    entity[ref] = typeof value === 'string' ? lookup.get(value) || value : value; // error
  });
}

不幸的是,这会引发编译器错误:

Type 'S | T[{ [K in keyof T]: S extends T[K] ? K : never; }[keyof T]]' is not assignable to type 'T[{ [K in keyof T]: S extends T[K] ? K : never; }[keyof T]]'.
  Type 'S' is not assignable to type 'T[{ [K in keyof T]: S extends T[K] ? K : never; }[keyof T]]'.
    Type 'Entity' is not assignable to type 'T[{ [K in keyof T]: S extends T[K] ? K : never; }[keyof T]]'.(2322)

我相信,借助 TypeScript 丰富的类型系统,应该完全有可能想出一个优雅的解决方案,但我似乎无法做到正确。我在这里错过了什么?

这里有一个link to the TypeScript Playground,供想要尝试的人使用。


请注意,我正在寻找一种优雅的意识形态 TypeScript 解决方案,而不是为了强制编译器的黑客攻击。如果我愿意,我不妨编写纯 JavaScript。

【问题讨论】:

    标签: typescript


    【解决方案1】:

    通常,当您在实现中的泛型函数中具有条件类型和映射类型时,TS 将很难从头到尾跟踪这些类型的所有含义。一般来说,仍然包含未解析类型参数的条件或映射类型只能分配给它自己,因此S 将不能分配给T[Ref&lt;T, S&gt;]

    有一种方法可以使函数类型的实现变得安全,但它是以开发人员经验为代价的:

    function link<S extends Entity, K extends PropertyKey>(target: Array<Record<K, string | S>>, source: S[], ref: K) { 
      const lookup = new Map(source.map<[string, S]>(entity => [entity.id, entity]));
    
      target.forEach(entity => {
        const value = entity[ref];
        entity[ref] = typeof value === 'string' ? lookup.get(value) || value : value;
      });
    }
    

    Playground Link

    ref 简化为一个简单的类型参数,并明确target 的项对于键K 具有S | string 类型的值,这将有助于编译器检查函数。该方法的缺点是:

    1. 你不会得到ref参数的代码完成,毕竟编译器只知道它是一个键,而不是它是什么对象的键。如果key 不是对象的一部分或没有S | string 类型,您将在target 参数上收到错误

    2. 您失去了传递对象字面量的能力(因为过多的属性检查)。编译器会抱怨 link([{group: "", a: ""}], groups, 'group'); a 不是已知属性。

    鉴于上述注意事项,我实际上会坚持使用您的版本,并使用type assertion

    或者您可以使用单独的实现签名,但不要指望 TS 对实际上归结为完全相同的两个签名非常严格(它会进行一些检查,但它们非常失败)

    function link<T extends Entity, S extends Entity>(target: T[], source: S[], ref: Ref<T, S>): void
    function link<S extends Entity, K extends PropertyKey>(target: Array<Record<K, string | S>>, source: S[], ref: K) { 
      const lookup = new Map(source.map<[string, S]>(entity => [entity.id, entity]));
    
      target.forEach(entity => {
        const value = entity[ref];
        entity[ref] = typeof value === 'string' ? lookup.get(value) || value : value;
      });
    }
    

    Playground Link

    【讨论】:

      【解决方案2】:

      将签名改为:

      function link<T extends Entity, K extends keyof T, S extends T[K] & Entity>(
        target: T[], source: S[], ref: K
      ) {
      

      TypeScript 无法推断出像 S 扩展 T[{ [K in keyof T]: S extends T[K] ? K : never; }[keyof T]] 这样复杂的关系。

      【讨论】:

        猜你喜欢
        • 2018-10-20
        • 2016-04-04
        • 1970-01-01
        • 2020-03-18
        • 2020-09-22
        • 2019-09-17
        • 2022-01-07
        • 2018-07-03
        • 1970-01-01
        相关资源
        最近更新 更多