← React & TypeScript

Fundamentos de TypeScript

¿Qué es un union type y qué es el narrowing en TypeScript?

Ver respuesta — intenta responderla en voz alta primero

Un union type dice que un valor puede ser de varios tipos, por ejemplo string | number. El narrowing es el proceso por el que TypeScript "estrecha" esa unión a un tipo concreto dentro de un bloque, usando comprobaciones como typeof, in o comparaciones.

Con una unión, TypeScript solo te deja usar lo que es seguro para todos los miembros. Para usar métodos específicos de uno, primero tienes que demostrar con código cuál es. Eso es el narrowing.

Formas típicas de estrechar:

  • typeof valor === "string": para primitivos.
  • "propiedad" in objeto: para distinguir objetos por sus propiedades.
  • Comparaciones con literales: if (estado === "error").
  • Uniones discriminadas: cada objeto de la unión lleva un campo literal común (el discriminante, típicamente tipo o kind). Un switch sobre ese campo estrecha automáticamente al tipo correcto.

Bonus de las uniones discriminadas: en el default del switch puedes asignar el valor a never. Si mañana alguien añade un caso a la unión y olvida manejarlo, el compilador avisa. A eso se le llama comprobación de exhaustividad.

// Unión simple + narrowing con typeof
function formatear(valor: string | number): string {
  if (typeof valor === "string") {
    return valor.toUpperCase(); // aquí es string
  }
  return valor.toFixed(2);      // aquí solo puede ser number
}

// Unión discriminada: el campo "tipo" es el discriminante
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 sabe que es Circulo
    case "cuadrado":
      return figura.lado ** 2;            // aquí es Cuadrado
    default: {
      // Exhaustividad: si añaden una figura y no la manejamos, esto no compila
      const nunca: never = figura;
      return nunca;
    }
  }
}

// Narrowing con "in" para objetos sin discriminante
type Perro = { ladrar: () => void };
type Gato = { maullar: () => void };

function hacerSonido(animal: Perro | Gato): void {
  if ("ladrar" in animal) animal.ladrar();
  else animal.maullar();
}

Usar un método específico sin estrechar antes:

function gritar(valor: string | number) {
  // return valor.toUpperCase();
  // Error: Property 'toUpperCase' does not exist on type 'string | number'.

  // Corrección: estrechar primero
  if (typeof valor === "string") return valor.toUpperCase();
  return String(valor);
}

El junior suele "resolverlo" con as string o any. Eso silencia el error pero puede explotar en runtime; el narrowing lo resuelve de forma segura.

"Un union type expresa que un valor puede ser de varios tipos, como string | number. TypeScript solo me deja usar lo común a todos, así que para acceder a lo específico hago narrowing: compruebo con typeof, con in o comparando literales, y dentro de ese bloque el compilador ya sabe el tipo exacto. Mi patrón favorito son las uniones discriminadas: cada variante lleva un campo literal como tipo, hago un switch sobre él, y en el default asigno a never para que, si alguien añade una variante nueva, el compilador me obligue a manejarla."

Reto rápido

Define una unión discriminada Respuesta con dos variantes: { estado: "ok"; datos: string[] } y { estado: "error"; mensaje: string }. Escribe procesar(respuesta: Respuesta): string que devuelva cuántos datos hay o el mensaje de error, con comprobación de exhaustividad.

Ver solución
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: {
      // Si se añade una variante nueva, esta línea deja de compilar
      const nunca: never = respuesta;
      return nunca;
    }
  }
}