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