How to create a circularly referenced type in TypeScript?
Asked Answered
M

6

69

I have the following code:

type Document = number | string | Array<Document>;

TypeScript complains with the following error:

test.ts(7,6): error TS2456: Type alias 'Document' circularly references itself.

Clearly circular references are not allowed. However, I still need this kind of structure. What would be a workaround for this?

Moreover answered 1/5, 2016 at 12:5 Comment(6)
Apparently, circular type references are permitted: #24444936Forfeit
I got quite confused why the union type here was wrapped in square brackets. Probably that wasn't intended. See my question here.Interlay
@Interlay You're right, fixing it now.Moreover
Native support for recursive types is coming in TypeScript 3.7, see: github.com/microsoft/TypeScript/pull/33050Emersion
Cool! I'll update the question when it comes out.Moreover
Hmm still doesn't seem to work in TS 4.3.5Effy
E
62

The creator of TypeScript explains how to create recursive types here.

The workaround for the circular reference is to use extends Array. In your case this would lead to this solution:

type Document = number | string | DocumentArray;

interface DocumentArray extends Array<Document> { }

Update (TypeScript 3.7)

Starting with TypeScript 3.7, recursive type aliases will be permitted and the workaround will no longer be needed. See: https://github.com/microsoft/TypeScript/pull/33050

Emersion answered 1/9, 2017 at 11:42 Comment(3)
Great, so I am not the only crazy person doing so in my code! :)Bubalo
I'm using TypeScript 4.3, and I still need this workaround for a more complex definition.Arteaga
It is related to this issue github.com/microsoft/TypeScript/issues/41164Arteaga
S
23

We already have good answers, but I think we can get closer to what you wanted in the first place:

You may try something like this:

interface Document {
    [index: number]: number | string | Document;
}

// compiles
const doc1: Document = [1, "one", [2, "two", [3, "three"]]];

// fails with "Index signatures are incompatible" which probably is what you want
const doc2: Document = [1, "one", [2, "two", { "three": 3 }]];

Compared to NPE's answer, you don't need wrapper objects around strings and numbers.

If you want a single number or string to be a valid document (which is not what you asked, but what NPE's answer implies), you may try this:

type ScalarDocument = number | string;
interface DocumentArray {
    [index: number]: ScalarDocument | DocumentArray;
}
type Document = ScalarDocument | DocumentArray;

const doc1: Document = 1;
const doc2: Document = "one";
const doc3: Document = [ doc1, doc2 ];

Update:

Using an interface with index signature instead of an array has the disadvantage of losing type information. Typescript won't let you call array methods like find, map or forEach. Example:

type ScalarDocument = number | string;
interface DocumentArray {
    [index: number]: ScalarDocument | DocumentArray;
}
type Document = ScalarDocument | DocumentArray;

const doc1: Document = 1;
const doc2: Document = "one";
const doc3: Document = [ doc1, doc2 ];
const doc = Math.random() < 0.5 ? doc1 : (Math.random() < 0.5 ? doc2 : doc3);

if (typeof doc === "number") {
    doc - 1;
} else if (typeof doc === "string") {
    doc.toUpperCase();
} else {
    // fails with "Property 'map' does not exist on type 'DocumentArray'"
    doc.map(d => d);
}

This can be solved by changing the definition of DocumentArray:

interface DocumentArray extends Array<ScalarDocument | DocumentArray> {}
Skier answered 24/1, 2017 at 11:4 Comment(1)
You may also fix certain indices to a specific type like so: interface DocumentArray { 0: number; [index: number]: ScalarDocument | DocumentArray; }Colossae
G
17

Here is one way to do it:

class Doc {
  val: number | string | Doc[];
}

let doc1: Doc = { val: 42 };
let doc2: Doc = { val: "the answer" };
let doc3: Doc = { val: [doc1, doc2] };

Types that reference themselves are known as "recursive types" and are discussed in section 3.11.8 of the language spec. The following excerpt explains why your attempt does not compile:

Classes and interfaces can reference themselves in their internal structure...

Your original example uses neither a class nor an interface; it uses a type alias.

Gory answered 1/5, 2016 at 13:38 Comment(1)
Thanks for explanation! This answer definitely deserves to be higher.Meld
I
11

As of Typescript 4, circular types are fixed for a bunch of things, but not for Record (and it's by design). Here's how you can do it if you come across this problem.

// This will fire a TS2456 error: Type alias "Tree" circularly reference itself
type Tree = Record<string, Tree | string>;
// No error
type Tree = {
    [key: string]: Tree | string;
};

ref: https://github.com/microsoft/TypeScript/pull/33050#issuecomment-543365074

Interfuse answered 28/12, 2021 at 0:37 Comment(0)
H
1

Building on what NPE said, types cannot recursively point to themselves, you could unroll this type to whatever level of depth you considered sufficient, e.g.:

type Document = [number|string|[number|string|[number|string|[number|string]]]]

Not pretty, but removes the need for an interface or class with a property value.

Hammy answered 1/5, 2016 at 14:52 Comment(1)
Indeed, I thought of that, but unfortunately I need it to be of infinite depth. Thanks for the answer anyways.Moreover
A
0

type Circular = {
    infinity: Map<string, Circular>
}

type Store = Circular['infinity']

declare var map:Store;

const foo = map.get('sd') // Circular | undefined
Adventurous answered 14/4, 2021 at 7:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.