TypeScript 中嵌套记录的递归类型

Ist*_*115 3 nested record typescript

从版本 3.7 开始,TypeScript 支持递归类型,我认为这将帮助我为嵌套记录结构创建正确的类型,但到目前为止,我还没有设法让编译器对我的尝试感到满意。

让我们忘记泛型,作为一个最小的例子,我有一个对象,它在字符串键后面的任意深度都有数值。

type NestedRecord = Record<string, number | NestedRecord>

const test: NestedRecord = {
  a: 10,
  b: {
    c: 20,
    d: 30,
  }
}
Run Code Online (Sandbox Code Playgroud)

这种类型对我来说是有意义的,但是编译器给了我以下错误:
Type alias 'NestedRecord' circularly references itself.

对递归类型的支持是否存在一些我不知道的限制?如果是这样,如何实现这种结构的类型?


编辑:根据@jcalz的回答,我最终使用了以下通用定义:

type NestedRecord<T> = { [key: string]: NestedRecordField<T> }
type NestedRecordField<T> = T | NestedRecord<T>
Run Code Online (Sandbox Code Playgroud)

在我的例子中,将它们分开是有意义的,因为对此类记录进行操作的递归函数NestedRecordField无论如何都需要类型,但以下一行解决方案也是有效的:

type NestedRecord<T> = { [key: string]: T | NestedRecord<T> }
Run Code Online (Sandbox Code Playgroud)

jca*_*alz 8

有关此问题的规范答案,请参阅microsoft/TypeScript#41164 。

TypeScript 允许一些循环定义,但有些是禁止的。type Oops = Oops例如,你不能拥有。如果您有像 这样的泛型类型,那么除非编译器愿意急切地用其定义替换type Foo<T> = ...,否则不清楚是否type Bar = Foo<Bar>允许或禁止。Foo编译器不会这样做;它推迟了这样的评估(以免编译器性能急剧下降)。type Bar = Foo<Bar>因此,即使事实证明 的定义是无害的,编译器也会拒绝Foo。这是 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
Run Code Online (Sandbox Code Playgroud)

然后你的作业就会按预期进行:

const test: NestedRecord = {
    a: 10,
    b: {
        c: 20,
        d: 30,
    }
}
Run Code Online (Sandbox Code Playgroud)

Playground 代码链接