简体   繁体   English

如何从键而不是值推断类型参数?

[英]How to infer type parameter from keys instead of values?

I have a class representing a directed graph structure, which is generic with one type parameter K extends string for the node names.我有一个 class 代表一个有向图结构,它是通用的,具有一个类型参数K extends string A graph is constructed by passing an object like {a: ['b'], b: []} which in this minimal example represents two nodes a and b , with one edge ab .一个图是通过传递一个 object 来构造的,例如{a: ['b'], b: []} ,在这个最小的例子中代表两个节点ab ,一条边ab

class Digraph<K extends string> {
    constructor(readonly adjacencyList: Record<K, K[]>) {}

    getNeighbours(k: K): K[] {
        return this.adjacencyList[k];
    }
}

However, declared like this, the type parameter K is inferred from the contents of the arrays, instead of from object's property names.然而,像这样声明,类型参数K是从 arrays 的内容中推断出来的,而不是从对象的属性名称中推断出来的。 This means K becomes 'b' instead of 'a' | 'b'这意味着K变为'b'而不是'a' | 'b' 'a' | 'b' , and so Typescript gives an error because it thinks a is an excess property in an object literal. 'a' | 'b' ,因此 Typescript 给出错误,因为它认为a是 object 文字中的多余属性。

// inferred as Digraph<'b'> instead of Digraph<'a' | 'b'>
// error: Argument of type '{ a: string[]; b: never[]; }' is not assignable to parameter of type 'Record<"b", "b"[]>'.
let digraph = new Digraph({
    a: ['b'],
    b: [],
});

Is there a way to have K inferred directly from the property names, instead of their values?有没有办法让K直接从属性名称而不是它们的值中推断出来?

Playground Link 游乐场链接


One solution I tried is to add another type parameter T extends Record<K, K[]> and declare constructor(readonly adjacencyList: T) {} .我尝试的一个解决方案是添加另一个类型参数T extends Record<K, K[]>并声明constructor(readonly adjacencyList: T) {} Then the excess property error goes away, but now K is only inferred as string .然后多余的属性错误消失了,但现在K只被推断为string

Also, the type Digraph<K, T> is too specific - two digraphs with the same nodes should be assignable to each other even when they have different edges, and I'd rather not have to write Digraph<K, Record<K, K[]>> or Digraph<K, any> to get around this.此外,类型Digraph<K, T>过于具体 - 具有相同节点的两个有向图应该可以相互分配,即使它们具有不同的边,我宁愿不必写Digraph<K, Record<K, K[]>>Digraph<K, any>来解决这个问题。 I'm looking for a solution which doesn't add an extra type parameter or change what K would be, if possible.如果可能的话,我正在寻找一种不添加额外类型参数或更改K的解决方案。

It seems you're looking for a NoInfer<T> type that states that T should only be used for type checking but not for inference, as discussed in this TypeScript issue .似乎您正在寻找一个NoInfer<T>类型,该类型声明T应该只用于类型检查而不是用于推理,正如TypeScript 问题中所讨论的那样。 It hasn't been added to TypeScript, but this definition from jcalz works for your code:它尚未添加到 TypeScript,但 jcalz 的此定义适用于您的代码:

type NoInfer<T> = [T][T extends any ? 0 : never];

If you rewrite the record as Record<K, NoInfer<K>[]> the class becomes如果将记录重写为Record<K, NoInfer<K>[]>则 class 变为

class Digraph<K extends string> {
    constructor(readonly adjacencyList: Record<K, NoInfer<K>[]>) {}

    getNeighbours(k: K): K[] {
        return this.adjacencyList[k];
    }
}

and the digraph example gets typed correctly:并且digraph示例被正确输入:

// inferred type: Digraph<"a" | "b">
let digraph = new Digraph({
    a: ['b'],
    b: [],
});

TypeScript playground TypeScript操场

Keep in mind it's still kind of a hack though, and future improvements might enable the type checker to infer NoInfer<T> = T and stop it from blocking inference.请记住,它仍然是一种 hack,未来的改进可能使类型检查器能够推断NoInfer<T> = T并阻止它阻止推断。

So your problem is that there are multiple inference site candidates for K in the type Record<K, K[]> , and that the compiler's inference algorithm is giving priority to the wrong one.所以你的问题是在Record<K, K[]>类型中有多个K的推理站点候选者,并且编译器的推理算法优先考虑错误的。 You would like to be able to tell the compiler that it should not use the second K (in the elements of the array in the property value position) for inference, and that it should only use the first K (in the property key position) for this purpose.您希望能够告诉编译器它不应该使用第二个K (在属性值位置的数组元素中)进行推理,并且它应该只使用第一个K (在属性键位置)以此目的。 It should only pay attention to that second site after K is inferred, and only to check that the inferred type works.它应该只注意推断K之后的第二个站点,并且只检查推断的类型是否有效。


There is an open issue at microsoft/TypeScript#14829 asking for such non-inferential type parameter usages . microsoft/TypeScript#14829有一个未解决的问题,要求使用此类非推理类型参数用法 The idea is that there should be some type function called NoInfer<T> where the type NoInfer<T> eventually evaluates just to T , but only after type inference has occurred.这个想法是应该有一些类型 function 称为NoInfer<T>类型NoInfer<T>最终仅计算为T ,但仅发生类型推断之后。 Then you would write this:然后你会这样写:

class Digraph<K extends string> {
    constructor(readonly adjacencyList: Record<K, NoInfer<K>[]>) { }

    getNeighbours(k: K): K[] {
        return this.adjacencyList[k];
    }
}

and everything should just work.一切都应该正常工作。


While no official version of NoInfer exists, there are some user-made implementations mentioned inside microsoft/TypeScript#14829 that work for some use cases.虽然没有官方版本的NoInfer ,但在 microsoft/TypeScript#14829 中提到了一些适用于某些用例的用户自定义实现。 The one I tend to recommend is:倾向于推荐的是:

type NoInfer<T> = [T][T extends any ? 0 : never];

Evaluation of the conditional type T extends any? 0: never条件类型T extends any? 0: never T extends any? 0: never is (currently for TS4.2) deferred until T is a specific type. T extends any? 0: never (目前为 TS4.2)推迟T为特定类型。 So while NoInfer<T> will eventually evaluate to T , the compiler cannot see this.因此,虽然NoInfer<T>最终将评估为T ,但编译器看不到这一点。

Hopefully microsoft/TypeScript#14829 will eventually get an official implementation, so that the workarounds mentioned in there can be abandoned in favor of it.希望 microsoft/TypeScript#14829 最终会得到官方实现,以便可以放弃其中提到的变通方法以支持它。 Or at least the existing workarounds would get promoted to supported features.或者至少现有的解决方法将被提升为受支持的功能。 (The type NoInfer<T> = T & {} version is about as supported as it can be , but unfortunately that will not work for your use case.) type NoInfer<T> = T & {}版本已得到尽可能多的支持,但不幸的是,这不适用于您的用例。)


Anyway, you can check that this definition of NoInfer<T> will behave as you want in your example code:无论如何,您可以检查NoInfer<T>的这个定义在您的示例代码中的行为是否符合您的要求:

let digraph = new Digraph({
    a: ['b'],
    b: [],
}); // okay, Digraph<"a" | "b">

let badDigraph = new Digraph({
    a: ['c'], // error, "c" is not assignable to "a" | "b"
    b: []
})

Playground link to code Playground 代码链接

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

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