我有一个表示有向图结构的类,它是通用的,具有一个类型参数
K extends string
作为节点名称。通过传递像 {a: ['b'], b: []}
这样的对象来构造图,在这个最小示例中,该对象表示两个节点 a 和 b,其中一条边 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
?
我尝试的一个解决方案是添加另一个类型参数
T extends Record<K, K[]>
并声明 constructor(readonly adjacencyList: T) {}
。然后多余的属性错误消失,但现在 K
只能推断为 string
。
此外,类型
Digraph<K, T>
太具体了 - 具有相同节点的两个有向图应该可以彼此分配,即使它们具有不同的边,而且我宁愿不必编写 Digraph<K, Record<K, K[]>>
或 Digraph<K, any>
来解决问题这。我正在寻找一种解决方案,如果可能的话,不会添加额外的类型参数或更改 K
的内容。
所以你的问题是,
K
类型中有多个Record<K, K[]>
的候选推理站点,并且编译器的推理算法优先考虑了错误的推理站点。 您希望能够告诉编译器它不应该使用第二个K
(在属性值位置的数组元素中)进行推理,而应该只使用第一个K
(在属性关键位置)用于此目的。 它应该只注意推断 K
之后的第二个站点,并且只检查推断的类型是否有效。
NoInfer<T>
实用程序类型来支持非推理类型参数用法。 类型 NoInfer<T>
最终仅计算为 T
,但仅在发生 之后 类型推断。 所以你可以这样写:
class Digraph<K extends string> {
constructor(readonly adjacencyList: Record<K, NoInfer<K>[]>) { }
getNeighbours(k: K): K[] {
return this.adjacencyList[k];
}
}
一切都会正常进行:
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: []
})
您似乎正在寻找一种
NoInfer<T>
类型,该类型声明 T
只能用于类型检查,而不应用于推理,如此 TypeScript 问题中所述。它尚未添加到 TypeScript,但来自 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: [],
});
请记住,它仍然是一种黑客行为,未来的改进可能使类型检查器能够推断
NoInfer<T> = T
并阻止它阻止推断。