Как создать циклически ссылочный тип в 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]]]]
Не очень, но устраняет необходимость в интерфейсе или классе со значением свойства.