英文:
Recursive type for nested record in TypeScript
问题
自 TypeScript 版本 3.7 起,它支持递归类型,这本应帮助我为嵌套的记录结构创建适当的类型,但到目前为止,我尝试让编译器满意都没有成功。
让我们不考虑泛型,以一个最简单的示例说,我有一个对象,在字符串键后面有任意深度的数字值。
type NestedRecord = Record<string, number | NestedRecord>;
const test: NestedRecord = {
a: 10,
b: {
c: 20,
d: 30,
}
}
这个类型对我来说是有意义的,但编译器给了我以下错误信息:
类型别名 'NestedRecord' 循环引用自身。
我是否没有意识到递归类型支持的某些限制?如果是这样,如何可以实现此结构的类型?
根据 @jcalz 的答案,我最终使用了以下泛型定义:
type NestedRecord<T> = { [key: string]: NestedRecordField<T> }
type NestedRecordField<T> = T | NestedRecord<T>
在我的情况下,将它们分开是有意义的,因为对此类记录执行操作的递归函数仍然需要 NestedRecordField
类型,但以下的一行解决方案也是有效的:
type NestedRecord<T> = { [key: string]: T | NestedRecord<T> }
英文:
Since version 3.7, TypeScript has support for recursive types, which I thought would help me create a proper type for a nested record structure, but so far I haven't managed to make the compiler happy with my attempts.
Let's forget about generics and say as a minimal example, that I have an object, which has numeric values at arbitrary depths behind string keys.
type NestedRecord = Record<string, number | NestedRecord>
const test: NestedRecord = {
a: 10,
b: {
c: 20,
d: 30,
}
}
This typing would make sense to me, but the compiler gives me the following error:
Type alias 'NestedRecord' circularly references itself.
Is there some limitation of the support for recursive types that I am not aware of? If so, how could a type for this structure be achieved?
Edit: Based on the answer of @jcalz I ended up using the following generic definition:
type NestedRecord<T> = { [key: string]: NestedRecordField<T> }
type NestedRecordField<T> = T | NestedRecord<T>
In my case separating these made sense, as a recursive function operating on such records needed the NestedRecordField
type anyway, but the following one line solution is also valid:
type NestedRecord<T> = { [key: string]: T | NestedRecord<T> }
答案1
得分: 3
请参阅 microsoft/TypeScript#41164 以获得此问题的规范答案。
TypeScript 允许一些循环定义,但一些被禁止。例如,你不能有 type Oops = Oops
。如果你有一个泛型 类型,像 type Foo<T> = ...
,那么除非编译器愿意急切地用它的定义来替换 Foo
,否则 type Bar = Foo<Bar>
是否被允许或被禁止是不清楚的。编译器不会这样做;它延迟这种评估(以免编译性能受到严重影响)。因此,即使最终发现 Foo
的定义是无害的,编译器也会拒绝 type Bar = Foo<Bar>
。这是 TypeScript 的设计限制。
所以,即使Record<K, V> 实用类型 在其 V
参数的递归类型中是无害的,编译器也无法注意到这一点,你会得到一个错误。
在这里推荐的方法是用其定义替换 Record<K, V>
,即 {[P in K]: V}
。对于 Record<string, V>
,它看起来像是 {[P in string]: V}
,它会评估为具有字符串索引签名的类型 {[k: string]: V}
。因此,你可以直接使用它:
type NestedRecord =
{ [k: string]: number | NestedRecord }; // okay
const test: NestedRecord = {
a: 10,
b: {
c: 20,
d: 30,
}
}
英文:
See microsoft/TypeScript#41164 for a canonical answer to this question.
TypeScript allows some circular definitions, but some are prohibited. You can't have type Oops = Oops
, for example. If you have a generic type like type Foo<T> = ...
, then it's not clear whether type Bar = Foo<Bar>
will be allowed or prohibited unless the compiler is willing to eagerly substitute Foo
with its definition. And the compiler doesn't do this; it defers such evaluation (lest compiler performance suffer dramatically). So the compiler will reject type Bar = Foo<Bar>
even if it turns out that the definition of Foo
is harmless. It's a design limitation of TypeScript.
So even though the Record<K, V>
utility type is harmless for recursive types in its V
parameter, the compiler fails to notice that, and you get an error.
The recommend approach here is to replace Record<K, V>
with its definition {[P in K]: V}
. For Record<string, V>
that would look like {[P in string]: V}
, which evaluates to a type with the string index signature {[k: string]: V}
. So you can use that directly:
type NestedRecord =
{ [k: string]: number | NestedRecord }; // okay
And then your assignment works as expected:
const test: NestedRecord = {
a: 10,
b: {
c: 20,
d: 30,
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论