209 votes

Comment vérifier qu'un bloc de commutation est exhaustif en TypeScript ?

J'ai un peu de 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');
}

J'ai oublié de m'occuper de la Color.Blue et j'aurais préféré obtenir une erreur de compilation. Comment puis-je structurer mon code pour que TypeScript signale cette erreur ?

5 votes

Je voulais juste informer les gens qu'il y a une solution en deux lignes de @Carlos Gines si vous faites défiler la page assez loin.

2 votes

La technologie TypeScript noImplicitReturns peut vous aider dans ce domaine.

230voto

Ryan Cavanaugh Points 17393

Pour ce faire, nous utiliserons l'option never (introduit dans TypeScript 2.0) qui représente des valeurs qui ne devraient pas exister.

La première étape consiste à écrire une fonction :

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

Utilisez-le ensuite dans le default (ou de manière équivalente, en dehors du commutateur) :

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

À ce stade, vous verrez une erreur :

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

Le message d'erreur indique les cas que vous avez oublié d'inclure dans votre commutateur exhaustif ! Si vous avez omis plusieurs valeurs, vous verrez apparaître une erreur concernant, par exemple, les cas suivants Color.Blue | Color.Yellow .

Notez que si vous utilisez strictNullChecks vous en aurez besoin return devant le assertUnreachable (sinon, c'est facultatif).

Tu peux être un peu plus fantaisiste si tu veux. Si vous utilisez une union discriminée, par exemple, il peut être utile de récupérer la propriété discriminante dans la fonction d'assertion à des fins de débogage. Cela ressemble à ceci :

// 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);
    }
}

Il s'agit d'un modèle intéressant, car vous bénéficiez d'une sécurité à la compilation pour vous assurer que vous avez traité tous les cas prévus. Et si vous obtenez une propriété vraiment hors du champ d'application (par exemple, un appelant JS a créé une nouvelle propriété de type species ), vous pouvez afficher un message d'erreur utile.

12 votes

Avec strictNullChecks n'est-il pas suffisant de définir le type de retour de la fonction en tant que string sans avoir besoin d'un assertUnreachable ne fonctionne pas du tout ?

2 votes

@dbandstra Vous pouvez certainement le faire, mais en tant que modèle générique, le assertUnreachable est plus fiable. Il fonctionne même sans strictNullChecks et continue également à fonctionner si, dans certaines conditions, vous souhaitez renvoyer undefined depuis l'extérieur du commutateur.

0 votes

Cela ne semble pas fonctionner lorsque l'enum est défini dans un fichier d.ts externe. En tout cas, je n'arrive pas à le faire fonctionner avec l'enum Office.MailboxEnums.RecipientType de l'API du module complémentaire Office JS de Microsoft.

107voto

Carlos Ginés Points 464

En complétant la réponse de Ryan, j'ai découvert que aquí qu'il n'y a pas besoin d'une fonction supplémentaire. Nous pouvons le faire directement :

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(`Unhandled color case: ${exhaustiveCheck}`);
  }
}

Vous pouvez le voir en action aquí dans TS Playground

Éditer : Inclusion d'une suggestion pour éviter les messages de linter "variable inutilisée".

89voto

Marcelo Lazaroni Points 3248

Vous n'avez pas besoin d'utiliser never ou ajouter quoi que ce soit à la fin de votre switch .

Si

  • Votre switch renvoie dans chaque cas
  • Vous avez le strictNullChecks drapeau de compilation de typescript activé
  • Votre fonction a un type de retour spécifié
  • Le type de retour n'est pas undefined o void

Vous obtiendrez une erreur si votre switch L'énoncé est non exhaustif, car il y aura des cas où rien ne sera retourné.

D'après votre exemple, si vous faites

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

Vous obtiendrez l'erreur de compilation suivante :

La fonction n'a pas de déclaration de retour final et le type de retour n'est pas n'inclut pas undefined .

60voto

drets Points 350

typescript-eslint a "contrôle d'exhaustivité dans le commutateur avec type d'union" règle :
@typescript-eslint/switch-exhaustiveness-check

Pour configurer cela, activez la règle dans package.json et activez le parseur TypeScript. Un exemple qui fonctionne avec React 17 :

"eslintConfig": {
    "rules": {
        "@typescript-eslint/switch-exhaustiveness-check": "warn"
    },
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "project": "./tsconfig.json"
    }
},

enter image description here

41voto

Martin Trummer Points 402

Solution

Ce que je fais est de définir une classe d'erreur :

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

et ensuite lancer cette erreur dans le cas par défaut :

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);
  }
}

Je pense que c'est plus facile à lire que l'approche par les fonctions recommandée par Ryan, parce que les throw a la coloration syntaxique par défaut.

Indice

El ts-essentiels La bibliothèque dispose d'une classe UnreachableCaseError exactement pour ce cas d'utilisation

Considérations relatives à l'exécution

Notez que le code typecript est transposé en javascript : Ainsi, tous les contrôles de type typecript ne fonctionnent qu'au moment de la compilation et les contrôles de type javascript ne fonctionnent pas. ne pas existent au moment de l'exécution : c'est-à-dire qu'il n'y a aucune garantie que la variable c est vraiment de type Color .
C'est différent des autres langages : par exemple, Java vérifiera également les types au moment de l'exécution et lancera une erreur significative si vous essayez d'appeler la fonction avec un argument de mauvais type - mais javascript ne le fait pas.

C'est la raison pour laquelle il est important de lancer une exception significative dans le processus d'exécution de l'opération. default cas : Stackblitz : lancer une erreur significative

Si vous ne l'avez pas fait, la fonction getColorName() retournerait implicitement undefined (lorsqu'il est appelé avec un argument inattendu) : Stackblitz : retour de n'importe quel

Dans les exemples ci-dessus, nous avons directement utilisé une variable de type any pour illustrer la question. Nous espérons que cela ne se produira pas dans des projets réels, mais il existe de nombreuses autres façons d'obtenir une variable d'un mauvais type au moment de l'exécution.
En voici quelques-unes, que j'ai déjà vues (et j'ai moi-même commis certaines de ces erreurs) :

  • l'utilisation de formulaires angulaires - ceux-ci ne sont pas sûrs du point de vue du type : toutes les valeurs de champ du formulaire sont de type any
    ng-forms Stackblitz exemple
  • tout implicite est autorisé
  • une valeur externe est utilisée et n'est pas validée (par exemple, la réponse http du serveur est simplement transmise à une interface).
  • nous lisons une valeur du local-storage qu'une ancienne version de l'application a écrite (ces valeurs ont changé, donc la nouvelle logique ne comprend pas l'ancienne valeur)
  • nous utilisons des librairies tierces qui ne sont pas sûres du point de vue du type (ou qui présentent simplement un bogue).

Ne soyez donc pas paresseux et écrivez ce cas supplémentaire par défaut - cela peut vous éviter bien des maux de tête...

0 votes

Notez que vous ne pouvez actuellement pas utiliser instanceof vérifie les sous-classes de Error : voir numéro 13965 qui mentionne également une solution de contournement

0 votes

Je l'ai vérifié et il fonctionne correctement maintenant. TypeScript 3.5.1.

1 votes

Ooooh, c'est magnifique, et aussi presque exactement la façon dont j'écrirais le code, si j'écrivais en simple JS.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X