Wie überprüfe ich, ob ein Switchblock in TypeScript vollständig ist?


97

Ich habe einen Code:

enum Color {
    Red,
    Green,
    Blue
}

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
        // Forgot about Blue
    }

    throw new Error('Did not expect to be here');
}

Ich habe vergessen, den Color.BlueFall zu behandeln , und ich hätte lieber einen Kompilierungsfehler erhalten. Wie kann ich meinen Code so strukturieren, dass TypeScript dies als Fehler kennzeichnet?


1
Ich wollte nur die Leute informieren, dass es eine zweizeilige Lösung von @Carlos Gines gibt, wenn Sie weit genug nach unten scrollen.
— Noumenon

Antworten:


122

Dazu verwenden wir den neverTyp (eingeführt in TypeScript 2.0), der Werte darstellt, die "nicht" auftreten sollten.

Der erste Schritt besteht darin, eine Funktion zu schreiben:

function assertUnreachable(x: never): never {
    throw new Error("Didn't expect to get here");
}

Verwenden Sie es dann in dem defaultFall (oder gleichwertig außerhalb des Schalters):

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
    }
    return assertUnreachable(c);
}

An dieser Stelle wird ein Fehler angezeigt:

return assertUnreachable(c);
       ~~~~~~~~~~~~~~~~~~~~~
       Type "Color.Blue" is not assignable to type "never"

Die Fehlermeldung zeigt die Fälle an, die Sie vergessen haben, in Ihren vollständigen Schalter aufzunehmen! Wenn Sie mehrere Werte weggelassen haben, wird ein Fehler angezeigt, z Color.Blue | Color.Yellow.

Beachten Sie, dass Sie dies vor dem Anruf strictNullChecksbenötigen , wenn Sie es verwenden (andernfalls ist es optional).returnassertUnreachable

Sie können ein wenig schicker werden, wenn Sie möchten. Wenn Sie beispielsweise eine diskriminierte Union verwenden, kann es hilfreich sein, die diskriminante Eigenschaft in der Assertionsfunktion für Debugging-Zwecke wiederherzustellen. Es sieht aus wie das:

// Discriminated union using string literals
interface Dog {
    species: "canine";
    woof: string;
}
interface Cat {
    species: "feline";
    meow: string;
}
interface Fish {
    species: "pisces";
    meow: string;
}
type Pet = Dog | Cat | Fish;

// Externally-visible signature
function throwBadPet(p: never): never;
// Implementation signature
function throwBadPet(p: Pet) {
    throw new Error('Unknown pet kind: ' + p.species);
}

function meetPet(p: Pet) {
    switch(p.species) {
        case "canine":
            console.log("Who's a good boy? " + p.woof);
            break;
        case "feline":
            console.log("Pretty kitty: " + p.meow);
            break;
        default:
            // Argument of type 'Fish' not assignable to 'never'
            throwBadPet(p);
    }
}

Dies ist ein schönes Muster, da Sie während der Kompilierung Sicherheit erhalten, um sicherzustellen, dass Sie alle Fälle behandelt haben, die Sie erwartet haben. Und wenn Sie eine Eigenschaft erhalten, die wirklich außerhalb des Gültigkeitsbereichs liegt (z. B. hat ein JS-Aufrufer eine neue erstellt species), können Sie eine nützliche Fehlermeldung ausgeben.


3
Wenn strictNullChecksaktiviert, reicht es nicht aus, den Rückgabetyp der Funktion als zu definieren string, ohne dass eine assertUnreachableFunktion erforderlich ist ?
— Dbandstra

1
@dbandstra Sie können das sicherlich tun, aber als generisches Muster ist assertUnreachable zuverlässiger. Es funktioniert auch ohne strictNullChecksund funktioniert auch weiterhin, wenn Sie unter bestimmten Bedingungen undefiniert von außerhalb des Switches zurückkehren möchten.
— Letharion

Dies scheint nicht zu funktionieren, wenn die Aufzählung in einer externen d.ts-Datei definiert ist. Zumindest kann ich es nicht mit der Aufzählung Office.MailboxEnums.RecipientType von der Microsoft Office JS-Add-In-API zum Laufen bringen.
— Søren Boisen

Die obige Lösung funktioniert für das grundlegende Beispiel in der ursprünglichen Beschreibung. Ich bin jedoch nicht sicher, ob es sich um ein allgemein anwendbares Muster handelt. Tauschen Sie die Farbaufzählung mit aus type InDiscriminant = { a: string } | { b: string }und wie kann man vorgehen? Ein einfacher Schalter kennt nicht mehr alle stringigen Werte, und es gibt keine gemeinsamen Eigenschaften zwischen den Mitgliedstypen der Union, über die unterschieden werden kann. Gibt es eine idiomatische Lösung für diesen Fall?
— cdaringe

noUnusedParametersDie Einrichtung von tsconfig löst den Fehler "x wird nie verwendet" aus, den Sie beheben können, indem Sie den Unterstrich function assertUnreachable(_x: never): never { throw new Error("Didn't expect to get here"); }
— voranstellen

32

Sie müssen neveram Ende nichts verwenden oder hinzufügen switch.

Wenn

  • Ihre switchAussage kehrt jeweils zurück
  • Sie haben das strictNullChecksTypenskript-Kompilierungsflag aktiviert
  • Ihre Funktion hat einen angegebenen Rückgabetyp
  • Der Rückgabetyp ist nicht undefinedodervoid

Sie erhalten eine Fehlermeldung, wenn Ihre switchAussage nicht vollständig ist, da in einem Fall nichts zurückgegeben wird.

Aus Ihrem Beispiel, wenn Sie dies tun

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
        // Forgot about Blue
    }
}

Sie erhalten den folgenden Kompilierungsfehler:

Der Funktion fehlt die End-Return-Anweisung, und der Rückgabetyp ist nicht enthalten undefined.


Diese Lösung erfordert die explizite Angabe aller möglichen Rückgabewerte einer Funktion. In anderen Fällen können wir sie weglassen und den Compiler die Typinferenz durchführen lassen.
— Aleksei

Ja, der Compiler wird sich beschweren, aber Sie sollten auch berücksichtigen, dass Sie zur Laufzeit immer noch einen falschen Wert erhalten könnten. In einem solchen Fall ziehe ich es vor, eine aussagekräftige Fehlermeldung
— auszulösen

6
Die Idee ist nicht zurückzukehren undefined, sondern den fehlenden Zweig der case-Anweisung zu erstellen. Auf diese Weise erhalten Sie zur Laufzeit keinen Fehler und müssen nichts werfen.
— Marcelo Lazaroni

3
Der Code funktioniert genau wie beschrieben. Wenn Sie den Typ ignorieren und bedenken, dass cdies alles sein könnte, gibt es keinen Grund, TypeScript überhaupt zu verwenden. Wenn cnull oder undefiniert sein könnte, sollte der Typ dies sagen und die Implementierung würde dies dann berücksichtigen.
— Marcelo Lazaroni

1
Diese Lösung löst einen Typoskriptfehler aus, der jedoch nicht immer eindeutig ist. Ich bevorzuge es, einen defaultFall mit einer nicht verwendeten Variablen zu verwenden, die wie neverein erklärender Kommentar eingegeben wurde "hey bud, if you're getting an error here, you forgot to add a case". YMMV.
— Matthias

20

Lösung

Ich definiere eine Fehlerklasse:

export class UnreachableCaseError extends Error {
  constructor(val: never) {
    super(`Unreachable case: ${JSON.stringify(val)}`);
  }
}

und werfen Sie dann diesen Fehler im Standardfall:

enum Color {
    Red,
    Green,
    Blue
}

function getColorName(c: Color): string {
  switch(c) {
      case Color.Red:
          return 'red, red wine';
      case Color.Green:
          return 'greenday';
      case Color.Blue:
          return "Im blue, daba dee daba";
      default:
          // Argument of type 'c' not assignable to 'never'
          throw new UnreachableCaseError(c);
  }
}

Ich denke, es ist einfacher zu lesen als der von Ryan empfohlene Funktionsansatz, da die throwKlausel die Standard-Syntaxhervorhebung aufweist.

Hinweis

Die ts-essentials- Bibliothek verfügt genau für diesen Anwendungsfall über die Klasse UnreachableCaseError

Überlegungen zur Laufzeit

Beachten Sie , dass Typoskript Code ist Javascript transpiled: So sind die alle Typoskript typechecks nur Arbeit bei der Kompilierung und nicht existieren zur Laufzeit: dh es keine Garantie dafür gibt , dass die Variable cwirklich vom Typ ist Color.
Dies unterscheidet sich von anderen Sprachen: Beispielsweise überprüft Java die Typen auch zur Laufzeit und würde einen aussagekräftigen Fehler auslösen, wenn Sie versuchen, die Funktion mit einem Argument vom falschen Typ aufzurufen - Javascript jedoch nicht.

Dies ist der Grund, warum es wichtig ist, eine sinnvolle Ausnahme in dem defaultFall auszulösen: Stackblitz: einen aussagekräftigen Fehler auslösen

Wenn Sie dies nicht tun, gibt die Funktion getColorName()implizit zurück undefined(wenn sie mit einem unerwarteten Argument aufgerufen wird): Stackblitz: Gibt eine zurück

In den obigen Beispielen haben wir direkt eine Variable vom Typ verwendet any, um das Problem zu veranschaulichen. Dies wird hoffentlich in realen Projekten nicht passieren - aber es gibt viele andere Möglichkeiten, wie Sie zur Laufzeit eine Variable eines falschen Typs erhalten können.
Hier sind einige, die ich bereits gesehen habe (und einige dieser Fehler selbst gemacht habe):

  • Verwendung von Winkelformen - diese sind nicht typsicher: Alle Formfeldwerte sind vom Typ any
    ng-Formulare Stackblitz-Beispiel
  • implizit ist erlaubt
  • Ein externer Wert wird verwendet und nicht validiert (z. B. wird die http-Antwort vom Server nur in eine Schnittstelle umgewandelt).
  • Wir lesen einen Wert aus dem lokalen Speicher, den eine ältere Version der App geschrieben hat (diese Werte haben sich geändert, sodass die neue Logik den alten Wert nicht versteht).
  • Wir verwenden einige Bibliotheken von Drittanbietern, die nicht typsicher sind (oder einfach einen Fehler haben).

Seien Sie also nicht faul und schreiben Sie diesen zusätzlichen Standardfall - er kann Ihnen viele Kopfschmerzen ersparen ...


Beachten Sie, dass Sie derzeit keine instanceofÜberprüfungen für Unterklassen von verwenden können Error: siehe Ausgabe Nr. 13965, in der auch eine
— Problemumgehung

Ich habe es überprüft und es funktioniert jetzt richtig. TypeScript 3.5.1.
— Aleksei

1
Ooooh, das ist wunderschön und auch fast genau so, wie ich den Code schreiben würde, wenn ich in einfachem JS schreiben würde.
— Pete

Wie findet der Compiler das?
— Jocull

@ Jocull nicht sicher, was Sie fragen. Vielleicht möchten Sie etwas über diskriminierte Gewerkschaften lesen ?
— TmTron

18

Aufbauend auf Ryans Antwort stellte ich hier fest, dass keine zusätzliche Funktion erforderlich ist. Wir können direkt tun:

function getColorName(c: Color): string {
  switch (c) {
    case Color.Red:
      return "red";
    case Color.Green:
      return "green";
    // Forgot about Blue
    default:
      const _exhaustiveCheck: never = c;
      throw new Error("How did we get here?");
  }
}

Sie können es hier auf dem TS-Spielplatz in Aktion sehen


2
Ich bevorzuge es auch, Variablen zu verwenden, anstatt eine Funktion zu erstellen (obwohl beide möglicherweise zur Bündelzeit entfernt werden). In jedem Fall kann sich Ihr Linter über eine nicht verwendete Variable beschweren, und Sie müssen darüber Folgendes hinzufügen:// eslint-disable-next-line @typescript-eslint/no-unused-vars
— Matthias

2
Oder Sie können die Variable in dem Fehler verwenden, den Sie throw new Error(`Unexpected: ${_exhaustiveCheck}`);
— auslösen

1
Gute Idee. Sie können auch eine anonyme Funktion definieren:default: ((x: never) => { throw new Error(c + " was unhandled."); })(c);
— Brady Holt

Ich kann bestätigen, dass die @ Nooodles-Lösung die SonarQube-Prüfung für nicht verwendete Variablen erfüllt. Vielen Dank. Liebe, wie knapp das ist.
— Noumenon

Ich habe einige verwandte (ich denke, dies könnten ziemlich häufige sein) Fehler aus dem obigen Code wie default: { const pleaseBeExhaustive: never = c; throw new Error(`Use all Color - ${pleaseBeExhaustive as string}`); }
— folgt


5

Als nette Wendung zu Ryans Antwort können Sie sie durch nevereine beliebige Zeichenfolge ersetzen , um die Fehlermeldung benutzerfreundlicher zu gestalten.

function assertUnreachable(x: 'error: Did you forget to handle this type?'): never {
    throw new Error("Didn't expect to get here");
}

Jetzt bekommen Sie:

return assertUnreachable(c);
       ~~~~~~~~~~~~~~~~~~~~~
       Type "Color.Blue" is not assignable to type "error: Did you forget to handle this type?"

Dies funktioniert, weil neveralles zugewiesen werden kann, einschließlich einer beliebigen Zeichenfolge.


3

Aufbauend auf den Antworten von Ryan und Carlos können Sie eine anonyme Methode verwenden, um zu vermeiden, dass Sie eine separate benannte Funktion erstellen müssen:

function getColorName(c: Color): string {
  switch (c) {
    case Color.Red:
      return "red";
    case Color.Green:
      return "green";
    // Forgot about Blue
    default:
      ((x: never) => {
        throw new Error(`${x} was unhandled!`);
      })(c);
  }
}

Wenn Ihr Switch nicht vollständig ist, wird ein Fehler bei der Kompilierung angezeigt.


2

So vermeiden Sie Typoskript- oder Linterwarnungen:

    default:
        ((_: never): void => {})(c);

im Zusammenhang:

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
        default:
            ((_: never): void => {})(c);
    }
}

Der Unterschied zwischen dieser Lösung und den anderen ist

  • Es gibt keine nicht referenzierten benannten Variablen
  • Es wird keine Ausnahme ausgelöst, da Typescript erzwingt, dass der Code nevertrotzdem ausgeführt wird

Erklären Sie im Detail, was der Code tut und wie er sich von anderen Antworten unterscheidet
— Sujitmohanty30

1

In wirklich einfachen Fällen, in denen Sie nur einen String nach Enum-Wert zurückgeben müssen, ist es (IMHO) einfacher, eine Konstante zum Speichern des Ergebniswörterbuchs zu verwenden, anstatt den Schalter zu verwenden. Zum Beispiel:

enum Color {
    Red,
    Green,
    Blue
}

function getColorName(c: Color): string {
  const colorNames: Record<Color, string> = {
    [Color.Red]: `I'm red`,
    [Color.Green]: `I'm green`,
    [Color.Blue]: `I'm blue, dabudi dabudai`,   
  }

  return colorNames[c] || ''
}

Hier müssen Sie also jeden Aufzählungswert als Konstante angeben, andernfalls erhalten Sie eine Fehlermeldung, z. B. wenn Blau fehlt:

TS2741: Die Eigenschaft 'Blau' fehlt im Typ '{[Color.Red]: string; [Color.Green]: string; ' aber erforderlich in Typ 'Datensatz'.

Es ist jedoch oft nicht der Fall und dann ist es wirklich besser, einen Fehler zu werfen, wie es Ryan Cavanaugh vorgeschlagen hat.

Außerdem war ich etwas verärgert, als ich feststellte, dass dies auch nicht funktioniert:

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
    }
    return '' as never // I had some hope that it rises a type error, but it doesn't :)
}

-1

Erstellen Sie eine benutzerdefinierte Funktion, anstatt eine switchAnweisung zu verwenden.

export function exhaustSwitch<T extends string, TRet>(
  value: T,
  map: { [x in T]: () => TRet }
): TRet {
  return map[value]();
}

Anwendungsbeispiel

type MyEnum = 'a' | 'b' | 'c';

const v = 'a' as MyEnum;

exhaustSwitch(v, {
  a: () => 1,
  b: () => 1,
  c: () => 1,
});

Wenn Sie später hinzufügen dzu MyEnum, erhalten Sie eine Fehlermeldung anProperty 'd' is missing in type ...


Dies funktioniert nur, wenn Ihre Enum-Werte Zeichenfolgen sind, da sie als Schlüssel im Objektparameter codiert werden müssen map.
— Will Madden
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.