typer - varför tillåter inte TypeScript cirkulära referenser i generiska typer?


Här är exemplen där typen direkt refererar till sig själv i sin definition, men när den abstraheres via en generisk misslyckas det helt.

type a = { val: a }; // <-- doesn't care about circular references!

type record<T> = { val: T };

type b = record<b>; // <-- doesn't work!

type func<T> = (arg: T) => void;

type c = func<c>; // <-- doesn't work!

type d = (arg: d) => void; // <-- works!?

Svar #1

Se microsoft/TypeScript#41164 för ett kanoniskt svar på denna fråga.

TypeScript tillåter cirkulära referenser i generiska gränssnitt och generiska klasser, eftersom gränssnitt och klassinstanser har statiskt kända egenskaps-/medlems-/metodnycklar och så sker all cirkularitet på "säkra" platser som egenskapsvärden eller metodparametrar eller returtyp.

interface Interface<T> { val: T }
type X = Interface<X> // okay

class Class<T> { method(arg: T): void { } }
type Y = Class<Y> // okay

Men för generiska typalias finns det ingen sådan garanti. Typalias kan ha vilken struktur som helst som vilken anonym typ som helst kan ha, så den potentiella cirkulariteten är inte begränsad till rekursiva trädliknande objekt:

type Safe<T> = { val: T };
type Unsafe<T> = T | { val: string };

När kompilatorn instansierar en generisk typ, skjuter den upp dess utvärdering; den försöker inte genast att fullständigt beräkna den resulterande typen. Allt den ser är formen:

type WouldBeSafe = Safe<WouldBeSafe>; 
type WouldBeUnsafe = Unsafe<WouldBeUnsafe>; 

Båda dessa ser likadana ut för kompilatorn... type X=SomeGenericTypeAlias<X>. Den kan inte "se" att WouldBeSafe skulle vara okej:

//type WouldBeSafe = { val: WouldBeSafe }; // would be okay

medan WouldBeUnsafe skulle vara ett problem:

//type WouldBeUnsafe = WouldBeUnsafe | { val: string }; // would be error

Eftersom det inte kan se skillnaden, och eftersom åtminstone vissa användningar skulle vara olagligt cirkulära, förbjuder det helt enkelt alla.


Så, vad kan du göra? Detta är ett av de fall där jag skulle föreslå att använda interface istället för type när du kan. Du kan skriva om din record-typ (ändra den till MyRecord av skäl för namngivningskonventioner) som ett interface och allt kommer att fungera:

interface MyRecord<T> { val: T };
type B = MyRecord<B>; // okay

Du kan till och med skriva om din func-typ (och ändra den till Func av namngivningsskäl igen) till ett interface genom att ändra funktionstypens uttryckssyntax till en samtalssignatursyntax:

interface Func<T> { (arg: T): void }
type C = Func<C>; // okay

Självklart finns det situationer där du inte kan göra det direkt, såsom den inbyggda Record-verktygstypen:

type Darn = Record<string, Darn>; // error

och du kan inte skriva om den mappade typen Record som ett interface. Och faktiskt skulle det vara osäkert att försöka göra nycklarna cirkulära, som type NoGood=Record<NoGood, string>. Om du bara vill göra Record<string, T> för generisk T, kan du skriva om det som ett interface:

interface Dictionary<T> extends Record<string, T> { };
type Works = Dictionary<Works>;

Så det finns ganska ofta ett sätt att använda ett interface istället för type för att låta dig uttrycka "säkra" rekursiva typer.

Playground-länk till kod

Svar #2

Låt oss dissekera dessa scenarier ett i taget.

Scenario 1

type a = { val: a }; // <-- doesn't care about circular references!

Det är intressant att detta är tillåtet. Jag förstår inte hur man skulle kunna konstruera en instans som skulle uppfylla denna typ:

const A: a = {
  val: {
    val: {
      // It will always error out at the most inner node.
    }
  }
}

Scenario 2

type record<T> = { val: T };

Detta är inte en cirkulär referens och kan uppfyllas på detta sätt:

const B: record<string> = {
  val: "test"
}

Scenario 3

type b = record<b>; // <-- doesn't work!

Det verkar rimligt för mig att detta inte fungerar. Precis som i Scenario 1 skulle det inte finnas något sätt att skapa en instans som uppfyller denna begränsning.

Scenario 4

type func<T> = (arg: T) => void;

Detta är inte en cirkulär referens och kan uppfyllas på detta sätt:

const C: func<string> = (arg: string) => {}

Scenario 5

type c = func<c>; // <-- doesn't work!

Det verkar rimligt för mig att detta inte fungerar. Precis som i Scenario 1 skulle det inte finnas något sätt att skapa en instans som uppfyller denna begränsning.

Scenario 6

type d = (arg: d) => void; // <-- works!?

Jag kan faktiskt skriva en funktion för att uppfylla denna begränsning, men jag är inte säker på vad den ger mig:

const D: d = (arg) => {}
D(D)

Relaterade artiklar