TypeScript 中嵌套记录的递归类型

huangapple go评论62阅读模式
英文:

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&lt;string, number | NestedRecord&gt;

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 &#39;NestedRecord&#39; 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&lt;T&gt; = { [key: string]: NestedRecordField&lt;T&gt; }
type NestedRecordField&lt;T&gt; = T | NestedRecord&lt;T&gt;

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&lt;T&gt; = { [key: string]: T | NestedRecord&lt;T&gt; }

答案1

得分: 3

请参阅 microsoft/TypeScript#41164 以获得此问题的规范答案。

TypeScript 允许一些循环定义,但一些被禁止。例如,你不能有 type Oops = Oops。如果你有一个泛型 类型,像 type Foo&lt;T&gt; = ...,那么除非编译器愿意急切地用它的定义来替换 Foo,否则 type Bar = Foo&lt;Bar&gt; 是否被允许或被禁止是不清楚的。编译器不会这样做;它延迟这种评估(以免编译性能受到严重影响)。因此,即使最终发现 Foo 的定义是无害的,编译器也会拒绝 type Bar = Foo&lt;Bar&gt;。这是 TypeScript 的设计限制。

所以,即使Record<K, V> 实用类型 在其 V 参数的递归类型中是无害的,编译器也无法注意到这一点,你会得到一个错误。

在这里推荐的方法是用其定义替换 Record&lt;K, V&gt;,即 {[P in K]: V}。对于 Record&lt;string, V&gt;,它看起来像是 {[P in string]: V},它会评估为具有字符串索引签名的类型 {[k: string]: V}。因此,你可以直接使用它:

type NestedRecord =
    { [k: string]: number | NestedRecord }; // okay

const test: NestedRecord = {
    a: 10,
    b: {
        c: 20,
        d: 30,
    }
}

在 Playground 上的代码链接

英文:

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&lt;T&gt; = ..., then it's not clear whether type Bar = Foo&lt;Bar&gt; 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&lt;Bar&gt; even if it turns out that the definition of Foo is harmless. It's a design limitation of TypeScript.

So even though the Record&lt;K, V&gt; 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&lt;K, V&gt; with its definition {[P in K]: V}. For Record&lt;string, V&gt; 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,
    }
}

Playground link to code

huangapple
  • 本文由 发表于 2023年6月29日 22:57:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/76582253.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定