[英]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.我将分别检索
Group
和Member
实体的列表。 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;
});
}
Simplifying ref
to be a simple type parameter and making clear that an item of target
has a value of type S | string
将
ref
简化为一个简单的类型参数,并明确target
项的值类型为S | string
S | string
for key K
will help the compiler check the function.键
K
的S | string
将帮助编译器检查 function。 The downsides of the approach are:该方法的缺点是:
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
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;
});
}
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.