¿Cómo verifico que un bloque de interruptor sea exhaustivo en TypeScript?

Resuelto Ryan Cavanaugh asked hace 8 años • 15 respuestas

Tengo un código:

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

Olvidé manejar el Color.Bluecaso y preferiría haber recibido un error de compilación. ¿Cómo puedo estructurar mi código de manera que TypeScript marque esto como un error?

Ryan Cavanaugh avatar Sep 10 '16 03:09 Ryan Cavanaugh
Aceptado

Para hacer esto, usaremos el nevertipo (introducido en TypeScript 2.0) que representa valores que "no deberían" ocurrir.

El primer paso es escribir una función:

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

Luego úselo en el defaultcaso (o equivalentemente, fuera del interruptor):

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

En este punto, verá un error:

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

¡El mensaje de error indica los casos que olvidó incluir en su cambio exhaustivo! Si dejó varios valores, verá un error sobre, por ejemplo Color.Blue | Color.Yellow, .

Tenga en cuenta que si está utilizando strictNullChecks, lo necesitará returnantes de la assertUnreachablellamada (de lo contrario, es opcional).

Puedes volverte un poco más elegante si quieres. Si está utilizando una unión discriminada, por ejemplo, puede resultar útil recuperar la propiedad discriminante en la función de aserción para fines de depuración. Se parece a esto:

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

Este es un buen patrón porque obtienes seguridad en tiempo de compilación para asegurarte de manejar todos los casos que esperabas. Y si obtiene una propiedad realmente fuera de alcance (por ejemplo, alguna persona que llama JS creó una nueva species), puede generar un mensaje de error útil.

Ryan Cavanaugh avatar Sep 09 '2016 20:09 Ryan Cavanaugh

Sobre la base de la respuesta de Ryan, descubrí aquí que no hay necesidad de ninguna función adicional. Podemos hacer directamente:

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

Puedes verlo en acción aquí en TS Playground

Editar: sugerencia incluida para evitar mensajes linter de "variables no utilizadas".

Carlos Ginés avatar Sep 19 '2019 11:09 Carlos Ginés

No es necesario utilizar neverni agregar nada al final de su archivo switch.

Si

  • Su switchestado de cuenta regresa en cada caso.
  • Tienes strictNullChecksactivado el indicador de compilación mecanografiado.
  • Su función tiene un tipo de retorno especificado
  • El tipo de devolución no es undefinedovoid

Recibirá un error si su switchdeclaración no es exhaustiva, ya que habrá casos en los que no se devolverá nada.

Según tu ejemplo, si lo haces

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

Obtendrá el siguiente error de compilación:

La función carece de declaración de devolución final y el tipo de devolución no incluye undefined.

Marcelo Lazaroni avatar Apr 05 '2019 10:04 Marcelo Lazaroni

typescript-eslinttiene la regla de "comprobación exhaustiva en el interruptor con tipo de unión" :
@typescript-eslint/switch-exhaustiveness-check

Para configurar esto, habilite la regla package.jsony habilite el analizador TypeScript. Un ejemplo que funciona con React 17:

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

ingrese la descripción de la imagen aquí

drets avatar Feb 11 '2020 10:02 drets