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 modo que TypeScript marque esto como un error?
typescript
Ryan Cavanaugh
fuente
fuente
Respuestas:
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 de manera equivalente, 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 exhaustivo cambio! Si deja varios valores, verá un error sobre, por ejemplo
Color.Blue | Color.Yellow
.Tenga en cuenta que si lo está utilizando
strictNullChecks
, lo necesitará antesreturn
de laassertUnreachable
llamada (de lo contrario, es opcional).Puedes ponerte un poco más elegante si quieres. Si está utilizando una unión discriminada, por ejemplo, puede ser ú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 patrón agradable porque obtienes seguridad en tiempo de compilación para asegurarte de manejar todos los casos que esperabas. Y si obtiene una propiedad verdaderamente fuera de alcance (por ejemplo, algún llamador JS creó una nueva
species
), puede lanzar un mensaje de error útil.fuente
strictNullChecks
habilitado, ¿no es suficiente definir el tipo de retorno de la función comostring
, sin necesidad de unaassertUnreachable
función en absoluto?strictNullChecks
y también sigue funcionando si hay condiciones en las que desea volver indefinido desde fuera del conmutador.type InDiscriminant = { a: string } | { b: string }
y ¿cómo se puede proceder? Un simple interruptor ya no conoce todos los valores fibrosos, y no hay propiedades comunes entre los tipos de miembros de la unión para discriminar. ¿Existe una solución idiomática para este caso?noUnusedParameters
La configuración de tsconfig arrojará un "x nunca se usa error" que puede corregirfunction assertUnreachable(_x: never): never { throw new Error("Didn't expect to get here"); }
No necesita usar
never
ni agregar nada al final de suswitch
.Si
switch
estado de cuenta regresa en cada casostrictNullChecks
activado el indicador de compilación de mecanografiadoundefined
ovoid
Obtendrá un error si su
switch
declaración no es exhaustiva, ya que habrá un caso en el que no se devolverá nada.De 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:
fuente
undefined
, sino crear la rama faltante del enunciado del caso. De esta manera, no obtendría un error en tiempo de ejecución y no es necesario lanzar nada.c
podría ser cualquier cosa, no hay razón para usar TypeScript en absoluto. Sic
pudiera ser nulo o indefinido, el tipo debería decir eso y la implementación lo tendría en cuenta.default
caso con una variable no utilizada escrita comonever
con un comentario que explique"hey bud, if you're getting an error here, you forgot to add a case"
. YMMV.Solución
Lo que hago es definir una clase de error:
export class UnreachableCaseError extends Error { constructor(val: never) { super(`Unreachable case: ${JSON.stringify(val)}`); } }
y luego lanza este error en el caso predeterminado:
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); } }
Creo que es más fácil de leer que el enfoque de función recomendado por Ryan, porque la
throw
cláusula tiene el resaltado de sintaxis predeterminado.Insinuación
La biblioteca ts-essentials tiene una clase UnreachableCaseError exactamente para este caso de uso
Consideraciones de tiempo de ejecución
Tenga en cuenta que el código mecanografiado se transpila a javascript: por lo tanto, todas las comprobaciones mecanografiadas sólo funcionan en tiempo de compilación y no existen en tiempo de ejecución: es decir, no hay garantía de que la variable
c
sea realmente de tipoColor
.Esto es diferente de otros lenguajes: por ejemplo, Java también verificará los tipos en tiempo de ejecución y arrojará un error significativo si intenta llamar a la función con un argumento de tipo incorrecto, pero javascript no lo hace.
Esta es la razón por la que es importante lanzar una excepción significativa en el
default
caso: Stackblitz: arrojar un error significativoSi no hiciste esto, la función
getColorName()
regresaría implícitamenteundefined
(cuando se llama con un argumento inesperado): Stackblitz: return anyEn los ejemplos anteriores, usamos directamente una variable de tipo
any
para ilustrar el problema. Es de esperar que esto no suceda en proyectos del mundo real, pero hay muchas otras formas de obtener una variable de un tipo incorrecto en tiempo de ejecución.Aquí hay algunos que ya he visto (y yo mismo cometí algunos de estos errores):
any
ng-forms Stackblitz ejemplo
Así que no sea perezoso y escriba este caso adicional predeterminado: puede evitarle muchos dolores de cabeza ...
fuente
instanceof
comprobaciones para subclases deError
: consulte el número 13965 que también menciona una solución alternativaSobre 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("How did we get here?"); } }
Puedes verlo en acción aquí en TS Playground
fuente
// eslint-disable-next-line @typescript-eslint/no-unused-vars
throw new Error(`Unexpected: ${_exhaustiveCheck}`);
default: ((x: never) => { throw new Error(c + " was unhandled."); })(c);
default: { const pleaseBeExhaustive: never = c; throw new Error(`Use all Color - ${pleaseBeExhaustive as string}`); }
typescript-eslint
tiene una regla de "verificación exhaustiva en el interruptor con tipo de unión" :@ typescript-eslint / switch-exhaustiveness-check
fuente
Como un buen giro en la respuesta de Ryan, puede reemplazarlo
never
con una cadena arbitraria para que el mensaje de error sea más fácil de usar.function assertUnreachable(x: 'error: Did you forget to handle this type?'): never { throw new Error("Didn't expect to get here"); }
Ahora obtienes:
return assertUnreachable(c); ~~~~~~~~~~~~~~~~~~~~~ Type "Color.Blue" is not assignable to type "error: Did you forget to handle this type?"
Esto funciona porque
never
se puede asignar a cualquier cosa, incluida una cadena arbitraria.fuente
Sobre la base de las respuestas de Ryan y Carlos , puede usar un método anónimo para evitar tener que crear una función con nombre separada:
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); } }
Si su cambio no es exhaustivo, obtendrá un error de tiempo de compilación .
fuente
Para evitar las advertencias de Typecript o linter:
default: ((_: never): void => {})(c);
en contexto:
function getColorName(c: Color): string { switch(c) { case Color.Red: return 'red'; case Color.Green: return 'green'; default: ((_: never): void => {})(c); } }
La diferencia entre esta solución y las demás es
never
ejecutará de todos modosfuente
En casos realmente simples, cuando solo necesita devolver una cadena por valor de enumeración, es más fácil (en mi humilde opinión) usar alguna constante para almacenar el diccionario de resultados en lugar de usar el interruptor. Por ejemplo:
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] || '' }
Entonces, aquí tendrá que mencionar cada valor de enumeración en constante, de lo contrario obtendrá un error como, por ejemplo, si falta Blue:
Sin embargo, a menudo no es el caso y entonces es realmente mejor lanzar un error como propuso Ryan Cavanaugh.
También me molestó un poco cuando descubrí que esto no funcionaría también:
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 :) }
fuente
Cree una función personalizada en lugar de utilizar una
switch
declaración.export function exhaustSwitch<T extends string, TRet>( value: T, map: { [x in T]: () => TRet } ): TRet { return map[value](); }
Uso de ejemplo
type MyEnum = 'a' | 'b' | 'c'; const v = 'a' as MyEnum; exhaustSwitch(v, { a: () => 1, b: () => 1, c: () => 1, });
Si luego agrega
d
aMyEnum
, recibirá un errorProperty 'd' is missing in type ...
fuente
map
.