¿Cómo verifico que un bloque de interruptor sea exhaustivo en TypeScript?
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.Blue
caso 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?
Para hacer esto, usaremos el never
tipo (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 default
caso (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á return
antes de la assertUnreachable
llamada (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.
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".
No es necesario utilizar never
ni agregar nada al final de su archivo switch
.
Si
- Su
switch
estado de cuenta regresa en cada caso. - Tienes
strictNullChecks
activado el indicador de compilación mecanografiado. - Su función tiene un tipo de retorno especificado
- El tipo de devolución no es
undefined
ovoid
Recibirá un error si su switch
declaració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
.
typescript-eslint
tiene 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.json
y 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"
}
},