How to create a circularly referenced type in TypeScript?
Solution 1:
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
Solution 2:
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> {}
Solution 3:
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.
Solution 4:
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.
Solution 5:
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