Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recursive type for nested record in TypeScript

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> }
like image 348
Isti115 Avatar asked Mar 30 '26 22:03

Isti115


1 Answers

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,
    }
}

Playground link to code

like image 164
jcalz Avatar answered Apr 02 '26 13:04

jcalz