Modelas tus estados con uniones discriminadas y aseguras su exhaustividad con el tipo never.
Abrir esta lección en Kodokonnever es el tipo vacío: ningún valor lo habita. Aparece cuando el compilador demuestra que una ruta de código es imposible, por ejemplo tras agotar cada miembro de una unión. Esa propiedad lo convierte en una herramienta de diseño: si logras que el compilador diga never en la rama por defecto de un switch, tienes una prueba estática de que todos los casos están gestionados. Esta es la base de la comprobación de exhaustividad.
Descubriste las uniones discriminadas en el módulo anterior; ahora es el momento de convertirlas en una auténtica herramienta de diseño. Frente a una única interfaz atiborrada de campos opcionales, una unión discriminada hace que los estados imposibles sean irrepresentables: no puedes tener a la vez data y error, ni un éxito sin datos. Es el patrón de referencia para los estados de carga, los resultados de operaciones y los mensajes de eventos.
type FetchState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function render(state: FetchState<string[]>): string {
switch (state.status) {
case "idle": return "Waiting";
case "loading": return "Loading…";
case "success": return state.data.join(", ");
case "error": return state.error.message;
}
}Ya te has cruzado con la guarda assertNever invocada en la rama default; disequemos ahora exactamente cómo funciona. Mientras cada caso esté cubierto, su parámetro tiene ahí el tipo never y todo compila. Añade una variante a la unión sin actualizar el switch, y la variante olvidada deja de ser asignable a never: error en tiempo de compilación, con el nombre del caso faltante en el mensaje. Conviertes un bug de producción en una compilación rechazada.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rect"; width: number; height: number };
function assertNever(value: never): never {
throw new Error("Unhandled case: " + String(value));
}
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
case "rect": return shape.width * shape.height;
default: return assertNever(shape);
}
}Dos sutilezas avanzadas merecen tu atención. Primero, la reducción por discriminante se propaga a las estructuras anidadas solo si compruebas directamente el objeto que las contiene; desestructurar el discriminante por su cuenta rompe el vínculo (salvo en los casos gestionados desde TypeScript 4.6 para la desestructuración conjunta). Segundo, never se absorbe en las uniones (string | never es simplemente string): esto es lo que permite a los tipos condicionales filtrar variantes, como hace Exclude.
type Event =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string };
type EventOf<T extends Event["type"]> =
Extract<Event, { type: T }>;
type ClickEvent = EventOf<"click">;
function handle<T extends Event["type"]>(
type: T,
handler: (event: EventOf<T>) => void
): void {
/* register the handler */
}
handle("click", (e) => console.log(e.x, e.y));Shape sin modificar el switch que contiene assertNever?string | never?neverstringunknownstring | undefined