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?

1
  • 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. Commented Apr 6, 2022 at 12:53

1 Answer 1

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

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you @Oblosys for the detailed explanation, I think that you can add the line: The reason why Record<string, StringOrRecordOfStrings> 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 Solid idea, done.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.