2

I've got two type definitions describing almost the same thing:

 // compilation fails with `Type alias 'StringOrRecordOfStrings' circularly references itself.ts(2456)`
type StringOrRecordOfStrings = string | string[] | Record<string, StringOrRecordOfStrings>;

 // compiles without problems
type StringOrRecordOfStrings = string | string[] | { [property: string]: StringOrRecordOfStrings };

Is anyone able to explain why the first type definition doesn't compile?

UltraMaster
  • 1,054
  • 1
  • 11
  • 23
  • Record and that type with an index signature are actually not equivalent and that may be why. If you look at the definition `Record` is a mapped type, which means it has to be expanded, but look! The value type is the alias! Then *that* has to be expanded as well, resulting in a circular reference. – kelsny Apr 06 '22 at 12:53

1 Answers1

2

The reason why Record<string, StringOrRecordOfStrings> is not allowed is that Record is a generic type, rather than a class or an interface.

There isn't a lot of clear documentation on this, but recursive type references for properties in objects, index signatures, and mapped types have been around for quite a while. The following works as early as TypeScript 3.3:

type Recursive = {p: Recursive} | {[p: string]: Recursive} | {[Key in 'a']: Recursive}

TypeScript 3.3 playground

This is why your second example type (with an index signature) checks.

TypeScript 3.7 extended the support for recursive references, as explained in this PR:

  • Instantiations of generic class and interface types (for example Array<Foo>).
  • Array types (for example Foo[]).
  • Tuple types (for example [string, Foo?]).

So now, also these three examples are valid:

type RecursiveCI = Promise<RecursiveCI>
type RecursiveT = [number, boolean, RecursiveT]
type RecursiveA = RecursiveA[]

I assume the sample is just test code, but you can get it to type check by using a helper interface like this:

type StringOrRecordOfStrings = string | string[] | Record<string, RecordInterface>

interface RecordInterface extends Record<string, StringOrRecordOfStrings> {}

TypeScript playground

Oblosys
  • 14,468
  • 3
  • 30
  • 38
  • Thank you @Oblosys for the detailed explanation, I think that you can add the line: `The reason why Record is not allowed is that Record is a generic type, rather than a class or an interface` To the top of your comment, that looks like exactly the TL;DR answer to my question. – UltraMaster Apr 07 '22 at 14:07
  • @UltraMaster Solid idea, done. – Oblosys Apr 07 '22 at 19:05