Kodokon kodokon.com

Narrowing: typeof, in, instanceof y type guards

Dominas los mecanismos de reducción de tipos (narrowing) y escribes tus propios predicados para hacer las uniones más fiables.

10 min · 3 preguntas

Abrir esta lección en Kodokon

El narrowing (reducción de tipos) es el mecanismo por el cual TypeScript refina un tipo amplio (una unión, unknown) hacia otro más preciso analizando el flujo de control. Ya lo usas sin pensarlo: cada if sobre un valor tipado dispara un análisis. El compilador sigue tres familias de guardas integradas: typeof para los primitivos, instanceof para las instancias de clase e in para la presencia de una propiedad. Elegirlas bien te ahorra los cast imprudentes con as, que desactivan la verificación en lugar de guiarla.

TYPESCRIPT
function formatValue(
  value: string | number | Date
): string {
  if (typeof value === "string") {
    return value.trim();
  }
  if (value instanceof Date) {
    return value.toISOString();
  }
  return value.toFixed(2);
}
Cada rama reduce el tipo; la última solo puede ser number.

El operador in es muy valioso cuando manejas objetos estructuralmente diferentes que no tienen ningún campo discriminante. Ten cuidado, sin embargo: in comprueba la presencia de una clave, incluidas las heredadas, no su tipo. Sobre datos externos (API, JSON), reduce según tus declaraciones, no según lo que realmente existe en tiempo de ejecución. La trampa clásica del mundo real: una comprobación typeof value === "object" deja pasar null, porque typeof null es "object" en JavaScript. Añade siempre una comprobación explícita de null.

TYPESCRIPT
interface Cat { meow(): void }
interface Dog { bark(): void }

function speak(animal: Cat | Dog): void {
  if ("meow" in animal) {
    animal.meow();
  } else {
    animal.bark();
  }
}
Reducción por presencia de propiedad con el operador in.

Cuando la lógica de la guarda se vuelve reutilizable o demasiado compleja para el análisis de flujo, escribe una type guard personalizada: una función cuyo tipo de retorno está anotado como value is T. Entonces el compilador confía en tu implementación. Es un contrato: si tu comprobación es incompleta, reintroduces errores en tiempo de ejecución que el sistema de tipos nunca verá. Desde TypeScript 5.5, los predicados simples se infieren automáticamente en callbacks como filter, pero la anotación explícita sigue siendo la norma para las funciones exportadas.

TYPESCRIPT
interface User { id: string; email: string }

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    typeof value.id === "string" &&
    "email" in value &&
    typeof value.email === "string"
  );
}

const raw: unknown =
  JSON.parse('{"id":"1","email":"a@b.c"}');
if (isUser(raw)) {
  console.log(raw.email.toLowerCase());
}
Un predicado que valida datos externos antes de usarlos.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Por qué la comprobación typeof value === "object" no basta para reducir a un objeto no nulo?
    • Porque typeof devuelve "Object" con mayúscula inicial
    • Porque typeof null también es "object" en JavaScript
    • Porque typeof no funciona con uniones
    • Porque los arrays devuelven "array"
  2. ¿Qué anotación de retorno convierte una función en una type guard personalizada?
    • boolean
    • value as User
    • value is User
    • asserts value
  3. ¿Cuál es el principal riesgo de una type guard personalizada mal escrita?
    • Un error en tiempo de compilación dentro de la función
    • Una reducción mentirosa: el compilador cree un tipo que es erróneo en tiempo de ejecución
    • Una pérdida de rendimiento de compilación