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

时间:2021-05-03 18:25:07

标签: typescript generics type-inference

我有一个表示有向图结构的类,它是通用的,节点名称有一个类型参数 K extends string。一个图是通过传递一个像 {a: ['b'], b: []} 这样的对象来构建的,在这个最小的例子中,它代表两个节点 ab,其中一个边 a > → b

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

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

然而,像这样声明,类型参数 K 是从数组的内容中推断出来的,而不是从对象的属性名称中推断出来的。这意味着 K 变为 'b' 而不是 'a' | 'b',因此 Typescript 会报错,因为它认为 a 是对象字面量中的多余属性。

// 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: [],
});

有没有办法让 K 直接从属性名称而不是它们的值推断出来?

Playground Link


我尝试过的一个解决方案是添加另一个类型参数 T extends Record<K, K[]> 并声明 constructor(readonly adjacencyList: T) {}。然后多余的属性错误消失了,但现在 K 仅被推断为 string

此外,类型 Digraph<K, T> 太具体了 - 即使具有不同的边,具有相同节点的两个有向图也应该可以相互分配,我宁愿不必写 Digraph<K, Record<K, K[]>>Digraph<K, any> 来解决这个问题。如果可能,我正在寻找一种不添加额外类型参数或更改 K 的解决方案。

2 个答案:

答案 0 :(得分:2)

您似乎正在寻找一种 NoInfer<T> 类型,该类型声明 T 应仅用于类型检查而不用于推理,如 this TypeScript issue 中所述。它尚未添加到 TypeScript,但 this definition from jcalz 适用于您的代码:

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

如果您将记录重写为 Record<K, NoInfer<K>[]>,则类变为

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

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

并且 digraph 示例被正确输入:

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

TypeScript playground

请记住,这仍然是一种黑客行为,未来的改进可能使类型检查器能够推断 NoInfer<T> = T 并阻止它阻止推断。

答案 1 :(得分:2)

因此,您的问题是 K 类型中的 Record<K, K[]> 有多个推理站点候选,并且编译器的推理算法优先考虑错误的一个。您希望能够告诉编译器它不应该使用第二个 K(在属性值位置的数组元素中)进行推理,而应该只使用第一个 {{1} }(在属性键位置)用于此目的。它应该只关注 K 被推断后的第二个站点,并且只检查推断的类型是否有效。


microsoft/TypeScript#14829 有一个未决问题,要求使用此类非推理类型参数。这个想法是应该有一些名为 K 的类型函数,其中类型 NoInfer<T> 最终仅计算为 NoInfer<T>,但仅在 after 类型推断发生后。然后你会这样写:

T

一切都应该正常工作。


虽然 class Digraph<K extends string> { constructor(readonly adjacencyList: Record<K, NoInfer<K>[]>) { } getNeighbours(k: K): K[] { return this.adjacencyList[k]; } } 官方版本不存在,但在 microsoft/TypeScript#14829 中提到了一些适用于某些用例的用户实现。我 tend to recommend 的那个是:

NoInfer

条件类型 type NoInfer<T> = [T][T extends any ? 0 : never]; 的评估(目前针对 TS4.2)推迟,直到 T extends any ? 0 : never 是特定类型。因此,虽然 T 最终会评估为 NoInfer<T>,但编译器看不到这一点。

希望 microsoft/TypeScript#14829 最终会得到官方实现,以便可以放弃其中提到的变通方法以支持它。或者至少现有的解决方法会被提升为支持的功能。 (T 版本是 about as supported as it can be,但遗憾的是这不适用于您的用例。)


无论如何,您可以在示例代码中检查 type NoInfer<T> = T & {} 的这个定义是否会按照您想要的方式运行:

NoInfer<T>

Playground link to code