What is a union type and what is narrowing in TypeScript?
Show answer — try answering out loud first
A union type says a value can be one of several types, for example string | number. Narrowing is the process by which TypeScript "narrows" that union to a concrete type within a block, using checks like typeof, in, or comparisons.
With a union, TypeScript only lets you use what's safe for all members. To use methods specific to one of them, you first have to prove with code which one it is. That's narrowing.
Typical ways to narrow:
typeof valor === "string": for primitives."propiedad" in objeto: to distinguish objects by their properties.- Comparisons with literals:
if (estado === "error"). - Discriminated unions: each object in the union carries a common literal field (the discriminant, typically
tipoorkind). Aswitchon that field automatically narrows to the correct type.
Bonus of discriminated unions: in the switch's default you can assign the value to never. If tomorrow someone adds a case to the union and forgets to handle it, the compiler warns you. This is called an exhaustiveness check.
// Simple union + narrowing with typeof
function formatear(valor: string | number): string {
if (typeof valor === "string") {
return valor.toUpperCase(); // here it's a string
}
return valor.toFixed(2); // here it can only be a number
}
// Discriminated union: the "tipo" field is the discriminant
type Circulo = { tipo: "circulo"; radio: number };
type Cuadrado = { tipo: "cuadrado"; lado: number };
type Figura = Circulo | Cuadrado;
function area(figura: Figura): number {
switch (figura.tipo) {
case "circulo":
return Math.PI * figura.radio ** 2; // TypeScript knows it's a Circulo
case "cuadrado":
return figura.lado ** 2; // here it's a Cuadrado
default: {
// Exhaustiveness: if a shape is added and we don't handle it, this won't compile
const nunca: never = figura;
return nunca;
}
}
}
// Narrowing with "in" for objects without a discriminant
type Perro = { ladrar: () => void };
type Gato = { maullar: () => void };
function hacerSonido(animal: Perro | Gato): void {
if ("ladrar" in animal) animal.ladrar();
else animal.maullar();
}
Using a specific method without narrowing first:
function gritar(valor: string | number) {
// return valor.toUpperCase();
// Error: Property 'toUpperCase' does not exist on type 'string | number'.
// Fix: narrow first
if (typeof valor === "string") return valor.toUpperCase();
return String(valor);
}
The junior often "solves it" with as string or any. That silences the error but can blow up at runtime; narrowing solves it safely.
"A union type expresses that a value can be one of several types, like
string | number. TypeScript only lets me use what's common to all of them, so to access the specific parts I do narrowing: I check withtypeof, within, or by comparing literals, and inside that block the compiler already knows the exact type. My favorite pattern is discriminated unions: each variant carries a literal field liketipo, I do a switch on it, and in the default I assign toneverso that, if someone adds a new variant, the compiler forces me to handle it."
Define a discriminated union Respuesta with two variants: { estado: "ok"; datos: string[] } and { estado: "error"; mensaje: string }. Write procesar(respuesta: Respuesta): string that returns how many items there are or the error message, with an exhaustiveness check.
See solution
type Respuesta =
| { estado: "ok"; datos: string[] }
| { estado: "error"; mensaje: string };
function procesar(respuesta: Respuesta): string {
switch (respuesta.estado) {
case "ok":
return `Llegaron ${respuesta.datos.length} datos`;
case "error":
return `Fallo: ${respuesta.mensaje}`;
default: {
// If a new variant is added, this line stops compiling
const nunca: never = respuesta;
return nunca;
}
}
}