简体   繁体   English

TypeScript 问题推断受限条件键类型的值类型

[英]TypeScript problem inferring value type of restricted conditional key type

Let's say that I have the following entity model:假设我有以下实体 model:

interface Entity {
  id: string;
}

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

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

I will be separately retrieving lists of both Group and Member entities.我将分别检索GroupMember实体的列表。 At retrieval, the entities will contain string references to other entities.在检索时,实体将包含对其他实体的字符串引用。 Eg, a Member entity will have a group property containing the ID of the group that the member belongs to.例如, Member实体将有一个group属性,其中包含该成员所属组的 ID。

Now, after retrieval, I want to hydrate/inflate the entities by replacing the string references with the entities that they refer to.现在,在检索之后,我想通过将字符串引用替换为它们所引用的实体来对实体进行水合/膨胀。 Eg, the following call should replace the group references in Member entities with the actual Group objects:例如,以下调用应将Member实体中的group引用替换为实际的Group对象:

link(members, groups, 'group');

Now, I want to restrict the third parameter in that call ( 'group' ) to only allow property names that can potentially refer to a Group entity, so I write the following type definition:现在,我想限制该调用中的第三个参数( 'group' )只允许可能引用Group实体的属性名称,因此我编写了以下类型定义:

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

This works correctly, eg type X = Ref<Member, Group> will evaluate to group .这可以正常工作,例如type X = Ref<Member, Group>将评估为group

The method implementation could then look like this:方法实现可能如下所示:

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

Unfortunately, this throws compiler errors:不幸的是,这会引发编译器错误:

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)

I believe that, with TypeScript's rich type system, it should be perfectly possible to come up with an elegant solution, but I can't seem to get it right.我相信,借助 TypeScript 丰富的类型系统,应该完全有可能提出一个优雅的解决方案,但我似乎无法做到正确。 What am I missing here?我在这里想念什么?

Here's a link to the TypeScript Playground for those who want to take a stab at it.是 TypeScript 游乐场的链接,供那些想要尝试的人使用。


Note that I'm looking for an elegant ideomatic TypeScript solution, and not for a hack to coerce the compiler.请注意,我正在寻找一个优雅的理念 TypeScript 解决方案,而不是为了强制编译器。 If I wanted that, I might as well write plain JavaScript.如果我想这样,我还不如写普通的 JavaScript。

Generally when you have conditional and mapped types in a generic function in the implementation TS will have a hard time following all the implications of those types all the way to the end.通常,当您在实现中的通用 function 中具有条件类型和映射类型时,TS 将很难一直跟踪这些类型的所有含义。 Generally a conditional or mapped type that still contains undresolved type parameters will only be assignable to itself, so S will not be assignable to T[Ref<T, S>] .通常,仍然包含未解析类型参数的条件或映射类型只能分配给自身,因此S不能分配给T[Ref<T, S>]

There is a way to make the implementation of the function type safe, but it comes as the cost of developer experience:有一种方法可以使 function 类型的实现安全,但它是以开发人员经验为代价的:

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 游乐场链接

Simplifying ref to be a simple type parameter and making clear that an item of target has a value of type S | stringref简化为一个简单的类型参数,并明确target项的值类型为S | string S | string for key K will help the compiler check the function.KS | string将帮助编译器检查 function。 The downsides of the approach are:该方法的缺点是:

  1. You will not get code completions for the ref parameter, after all the compiler only knows it's a key, not the key of what object it is.你不会得到ref参数的代码完成,毕竟编译器只知道它是一个键,而不是 object 的键。 You will get errors on the target parameter if the key is not part of the object or does not have type S | string如果key不是 object 的一部分或不具有类型S | string ,您将在target参数上遇到错误 | S | string

  2. You lose the ability to pass in object literals (because of excess property checks).您失去了传递 object 文字的能力(因为过多的属性检查)。 The compiler will complain that for link([{group: "", a: ""}], groups, 'group');编译器会抱怨 for link([{group: "", a: ""}], groups, 'group'); a is not a known property. a不是已知属性。

Given the caveats above, I would actually stick with your version, and use a type assertion .鉴于上述警告,我实际上会坚持使用您的版本,并使用type assertion

Or you could use a separate implementation signature, but don't expect that TS is very strict about the two signatures actually boiling down to the exact same thing (it does some checks but they are quite lose)或者您可以使用单独的实现签名,但不要指望 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 游乐场链接

Change the signature to:将签名更改为:

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

TypeScript isn't able to infer a relationship as complex as S extending T[{ [K in keyof T]: S extends T[K]? K: never; }[keyof T]] TypeScript 无法推断出像S扩展T[{ [K in keyof T]: S extends T[K]? K: never; }[keyof T]] T[{ [K in keyof T]: S extends T[K]? K: never; }[keyof T]] T[{ [K in keyof T]: S extends T[K]? K: never; }[keyof T]] . T[{ [K in keyof T]: S extends T[K]? K: never; }[keyof T]]

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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