См. microsoft/TypeScript#41164 для получения канонического ответа на этот вопрос.
Машинопись допускает циклические ссылки в универсальных интерфейсах и универсальных классах, поскольку интерфейсы и экземпляры классов имеют статически известные ключи свойств/элементов/методов, и поэтому любая цикличность происходит в "безопасных" местах, таких как значения свойств или параметры метода или тип возвращаемого значения.
interface Interface<T> { val: T }
type X = Interface<X> // okay
class Class<T> { method(arg: T): void { } }
type Y = Class<Y> // okay
Но для общих псевдонимов типов такой гарантии нет. Псевдонимы типов могут иметь любую структуру, которую может иметь любой анонимный тип, поэтому потенциальная цикличность не ограничивается рекурсивными древовидными объектами:
type Safe<T> = { val: T };
type Unsafe<T> = T | { val: string };
Когда компилятор создает экземпляр универсального типа, он откладывает его оценку; он не сразу пытается полностью вычислить результирующий тип. Все, что он видит, - это форма:
type WouldBeSafe = Safe<WouldBeSafe>;
type WouldBeUnsafe = Unsafe<WouldBeUnsafe>;
Оба они выглядят одинаково для компилятора... type X = SomeGenericTypeAlias<X>
. Он не может "видеть", что WouldBeSafe
все было бы в порядке:
//type WouldBeSafe = { val: WouldBeSafe }; // would be okay
пока WouldBeUnsafe
это было бы проблемой:
//type WouldBeUnsafe = WouldBeUnsafe | { val: string }; // would be error
Поскольку он не видит разницы и поскольку, по крайней мере, некоторые виды использования были бы незаконно запрещены, он просто запрещает их все.
Итак, что вы можете сделать? Это один из тех случаев, когда я бы предложил использовать interface
вместо type
когда сможешь. Вы можете переписать свой record
введите (изменив его на MyRecord
по соображениям соглашения об именах) в качестве interface
и все будет работать:
interface MyRecord<T> { val: T };
type B = MyRecord<B>; // okay
Вы даже можете переписать свой func
введите (изменив его на Func
опять же по соображениям соглашения об именах) в качестве interface
изменив синтаксис выражения типа функции на синтаксис подписи вызова:
interface Func<T> { (arg: T): void }
type C = Func<C>; // okay
Конечно, бывают ситуации, когда вы не можете сделать это напрямую, например, встроенныйRecord
тип полезного использования:
type Darn = Record<string, Darn>; // error
и вы не можете переписать сопоставленный тип Record
в качестве interface
. И действительно, было бы небезопасно пытаться сделать ключи круговыми, как type NoGood = Record<NoGood, string>
. Если вы только хотите сделать Record<string, T>
для общего T
, вы можете переписать это как interface
:
interface Dictionary<T> extends Record<string, T> { };
type Works = Dictionary<Works>;
Так что довольно часто есть способ использовать interface
вместо type
чтобы позволить вам выражать "безопасные" рекурсивные типы.
Ссылка на игровую площадку для кода