Как создать циклически ссылочный тип в TypeScript?

У меня есть следующий код:

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

TypeScript жалуется на следующую ошибку:

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

Явные циркулярные ссылки не допускаются. Однако мне все еще нужна такая структура. Что бы обойти это?

Ответ 1

Создатель TypeScript объясняет, как создавать рекурсивные типы здесь: https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540

Обходной путь для круговой ссылки должен использовать extends Array. В вашем случае это приведет к такому решению:


type Document = number | string | DocumentArray;

interface DocumentArray extends Array<Document> { }

Обновление (TypeScript 3.7)

Начиная с TypeScript 3.7, рекурсивные псевдонимы типов будут разрешены, и обходной путь больше не потребуется. Видеть: https://github.com/microsoft/TypeScript/pull/33050

Ответ 2

У нас уже есть хорошие ответы, но я думаю, что мы можем приблизиться к тому, что вам нужно в первую очередь:

Вы можете попробовать что-то вроде этого:

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 }]];

По сравнению с ответом NPE вам не нужны объекты-оболочки вокруг строк и чисел.

Если вы хотите, чтобы один номер или строка являлся допустимым документом (это не то, что вы просили, но какой ответ NPE подразумевает), вы можете попробовать следующее:

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:

Использование интерфейса с сигнатурой индекса вместо массива имеет недостаток потери информации о типе. Typescript не позволит вам вызвать методы массива, такие как find, map или forEach. Пример:

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);
}

Это можно решить, изменив определение DocumentArray:

interface DocumentArray extends Array<ScalarDocument | DocumentArray> {}

Ответ 3

Вот один из способов сделать это:

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

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

Типы, которые ссылаются сами, называются "рекурсивными типами" и обсуждаются в разделе раздел 3.11.8 спецификации языка. Следующая выдержка объясняет, почему ваша попытка не компилируется:

Классы и интерфейсы могут ссылаться на себя во внутренней структуре...

В вашем исходном примере не используется ни класс, ни интерфейс; он использует псевдоним типа.

Ответ 4

Основываясь на том, что сказал NPE, типы не могут рекурсивно указывать на себя, вы можете развернуть этот тип до любого уровня глубины, который считаете достаточным, например:

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

Не очень, но устраняет необходимость в интерфейсе или классе со значением свойства.