(More) Recursive Type Aliases

Playground

Type aliases have always had a limitation in how they could be “recursively” referenced.The reason is that any use of a type alias needs to be able to substitute itself with whatever it aliases.In some cases, that’s not possible, so the compiler rejects certain recursive aliases like the following:

  1. type Foo = Foo;

This is a reasonable restriction because any use of Foo would need to be replaced with Foo which would need to be replaced with Foo which would need to be replaced with Foo which… well, hopefully you get the idea!In the end, there isn’t a type that makes sense in place of Foo.

This is fairly consistent with how other languages treat type aliases, but it does give rise to some slightly surprising scenarios for how users leverage the feature.For example, in TypeScript 3.6 and prior, the following causes an error.

  1. type ValueOrArray<T> = T | Array<ValueOrArray<T>>;
  2. // ~~~~~~~~~~~~
  3. // error: Type alias 'ValueOrArray' circularly references itself.

This is strange because there is technically nothing wrong with any use users could always write what was effectively the same code by introducing an interface.

  1. type ValueOrArray<T> = T | ArrayOfValueOrArray<T>;
  2. interface ArrayOfValueOrArray<T> extends Array<ValueOrArray<T>> {}

Because interfaces (and other object types) introduce a level of indirection and their full structure doesn’t need to be eagerly built out, TypeScript has no problem working with this structure.

But workaround of introducing the interface wasn’t intuitive for users.And in principle there really wasn’t anything wrong with the original version of ValueOrArray that used Array directly.If the compiler was a little bit “lazier” and only calculated the type arguments to Array when necessary, then TypeScript could express these correctly.

That’s exactly what TypeScript 3.7 introduces.At the “top level” of a type alias, TypeScript will defer resolving type arguments to permit these patterns.

This means that code like the following that was trying to represent JSON…

  1. type Json =
  2. | string
  3. | number
  4. | boolean
  5. | null
  6. | JsonObject
  7. | JsonArray;
  8. interface JsonObject {
  9. [property: string]: Json;
  10. }
  11. interface JsonArray extends Array<Json> {}

can finally be rewritten without helper interfaces.

  1. type Json =
  2. | string
  3. | number
  4. | boolean
  5. | null
  6. | { [property: string]: Json }
  7. | Json[];

This new relaxation also lets us recursively reference type aliases in tuples as well.The following code which used to error is now valid TypeScript code.

  1. type VirtualNode =
  2. | string
  3. | [string, { [key: string]: any }, ...VirtualNode[]];
  4. const myNode: VirtualNode =
  5. ["div", { id: "parent" },
  6. ["div", { id: "first-child" }, "I'm the first child"],
  7. ["div", { id: "second-child" }, "I'm the second child"]
  8. ];

For more information, you can read up on the original pull request.