javascript - Variabel av unionstyp orsakar fel i switch-sats


Tänk att vi har en unionstyp som representerar ett av tre olika strängvärden.

type Animal = 'bird' | 'cat' | 'dog';

Nu skulle jag vilja skapa en hund och kolla vilken typ av djur det är för att skapa det korrekta ljudet den gör.

let oscar: Animal = 'dog';

switch (oscar) {
  case 'bird':
    console.log('tweet');
    break;
  case 'cat':
    console.log('meow');
    break;
  case 'dog':
    console.log('bark');
    break;
}

Denna kod kommer att resultera i ett TypeScript-fel: Typ '"bird"' kan inte jämföras med typ '"dog"'.ts(2678) (analogt med katt). Men om jag använder en explicit typomvandling på variabeln oscar fungerar det utan problem:

switch (oscar as Animal) {
  case 'bird':
    ...
  case 'cat':
    ...
  case 'dog':
    ...
}

Kan du vänligen förklara för mig varför de första två switch-satserna misslyckas om jag använder ett explicit värde för oscar?

Jag skulle kunna förstå felet om jag deklarerade Oscar som en konstant: const oscar='dog';, eftersom det i så fall alltid skulle vara en hund och inget annat. Föreställ dig dock för ett ögonblick att Oscar kunde bli en katt om en trollkarl utförde en viss förtrollning:

let oscar: Animal = 'dog';

while(true) {
  switch (oscar) {
  case 'bird':
    ...
  case 'cat':
    ...
  case 'dog':
    console.log('bark');

    // here comes the wizard
    if(wizard.performsSpell('makeOscarBecomeACat')) {
      oscar = 'cat';  // that should be valid, because oscar is of type Animal
    }

    break;
  }
}

Missförstår jag något om tilldelningen av variabeln oscar, eller är detta helt enkelt en TypeScript-bugg?

Svar #1

Vad du kanske missförstår är att TypeScript 2.0 och senare har en funktion som kallas kontrollflödesbaserad typanalys, implementerad i microsoft/TypeScript#8010. En av effekterna av denna funktion är att

En tilldelning (inklusive en initialiserare i en deklaration) av ett värde av typen S till en variabel av typen T ändrar typen på den variabeln till T begränsad av S i kodvägen som följer efter tilldelningen. [...] Typen T begränsad av S beräknas enligt följande: [...] Om T är en unionstyp, är resultatet unionen av varje deltyp i T till vilken S kan tilldelas.

Det betyder uttalandet

let oscar: Animal = 'dog';

tolkas som: "variabeln oscar har typen Animal, en unionstyp. Den har tilldelats ett värde av strängliteraltypen "dog", så tills den tilldelas ett nytt värde kommer vi att behandla variabeln oscar som typen Animal begränsad av "dog", vilket bara är "dog".

Och därför i din switch/case-sats:

case 'bird': // error!
//   ~~~~~~ <-- Type '"bird"' is not comparable to type '"dog"'

Du får felet om att försöka jämföra en strängliteral "bird" med en strängliteral "dog". Kompilatorn vet att fallet 'bird' är omöjligt eftersom du inte har tilldelat oscar till något som är kompatibelt med 'bird'.

Även i ditt wizard-fall förstår kompilatorn att när den når switch/case-satserna kan oscar bara vara "cat" eller "dog" och inte "bird":

case 'bird': // error! 
//   ~~~~~~ <-- Type '"bird"' is not comparable to type '"cat" | "dog"'

Detta är förmodligen alla goda nyheter; kompilatorn fångar fall som aldrig kan hända. För många situationer är detta verkliga buggar.

Om du vill att kompileraren inte ska inse att oscar definitivt är "dog" och bara veta att det är ett Animal (som, säg, en platshållare tills du skriver kod som gör det verkligen möjligt att det kan vara vilken medlem som helst av Animal), kan du använda en typkonstatering i själva tilldelningen:

let oscar: Animal = 'dog' as Animal;

Nu kommer all din annan kod att kompileras utan fel. Du kan till och med glömma bort annoteringen eftersom den ändå inte hjälpte dig:

let oscar = 'dog' as Animal;

Okej, hoppas det hjälper; lycka till!

Lekplatslänk till kod

Relaterade artiklar