En esta página
Narrowing y type guards
¿Qué es el narrowing?
Cuando declaras una variable de tipo string | number, TypeScript sabe que puede ser cualquiera de los dos. Narrowing es el proceso mediante el cual el compilador reduce (estrecha) ese tipo a uno más específico dentro de un bloque de código, basándose en las comprobaciones que tú escribes.
El resultado es que dentro de cada rama, TypeScript te ofrece el autocompletado y la seguridad de tipos del tipo concreto, sin necesidad de un cast explícito.
typeof narrowing
La comprobación más básica usa el operador typeof, que devuelve un string con el nombre del tipo primitivo:
function formatearValor(valor: string | number | boolean): string {
if (typeof valor === "string") {
// Aquí TypeScript sabe que valor es string
return valor.toUpperCase();
}
if (typeof valor === "number") {
// Aquí es number
return valor.toFixed(2);
}
// Aquí es boolean (TypeScript lo deduce por exhaustividad)
return valor ? "Sí" : "No";
}
console.log(formatearValor("hola")); // "HOLA"
console.log(formatearValor(3.14159)); // "3.14"
console.log(formatearValor(false)); // "No"Los tipos reconocidos por typeof son: "string", "number", "bigint", "boolean", "symbol", "undefined", "object" y "function".
Truthiness narrowing
JavaScript tiene valores falsy: 0, "", null, undefined, NaN, false. TypeScript entiende las comprobaciones de veracidad:
function procesarTexto(texto: string | null | undefined): string {
if (!texto) {
// texto es null, undefined o string vacío
return "(sin contenido)";
}
// Aquí texto es string (y no vacío)
return texto.trim().toLowerCase();
}También puedes usar el operador de coalescencia nula (??) para un narrowing más explícito:
function obtenerLongitud(valor: string | null): number {
// Si valor es null, devuelve 0
return (valor ?? "").length;
}Equality narrowing
TypeScript estrecha tipos cuando comparas con === o !==:
type Direccion = "norte" | "sur" | "este" | "oeste";
function mover(dir: Direccion, pasos: number): string {
if (dir === "norte" || dir === "sur") {
return `Moviéndose ${pasos} pasos en el eje Y (${dir})`;
}
// Aquí TypeScript sabe que dir es "este" | "oeste"
return `Moviéndose ${pasos} pasos en el eje X (${dir})`;
}El operador `in`
Cuando trabajas con objetos de diferentes formas, el operador in verifica si una propiedad existe y TypeScript lo usa para estrechar:
interface Circulo {
radio: number;
}
interface Rectangulo {
ancho: number;
alto: number;
}
type Forma = Circulo | Rectangulo;
function calcularArea(forma: Forma): number {
if ("radio" in forma) {
// forma es Circulo
return Math.PI * forma.radio ** 2;
}
// forma es Rectangulo
return forma.ancho * forma.alto;
}
console.log(calcularArea({ radio: 5 })); // ~78.54
console.log(calcularArea({ ancho: 4, alto: 6 })); // 24instanceof narrowing
Para clases e instancias, instanceof es el mecanismo ideal:
class ErrorDeRed extends Error {
constructor(
public readonly statusCode: number,
mensaje: string
) {
super(mensaje);
this.name = "ErrorDeRed";
}
}
class ErrorDeValidacion extends Error {
constructor(
public readonly campo: string,
mensaje: string
) {
super(mensaje);
this.name = "ErrorDeValidacion";
}
}
function manejarError(error: unknown): string {
if (error instanceof ErrorDeRed) {
return `HTTP ${error.statusCode}: ${error.message}`;
}
if (error instanceof ErrorDeValidacion) {
return `Campo "${error.campo}" inválido: ${error.message}`;
}
if (error instanceof Error) {
return `Error genérico: ${error.message}`;
}
return "Error desconocido";
}Discriminated unions
Las uniones discriminadas son el patrón más poderoso para el narrowing. Consisten en una unión de objetos que comparten una propiedad literal (el discriminador) con valores únicos por variante:
type EstadoPedido =
| { estado: "pendiente"; fechaCreacion: Date }
| { estado: "procesando"; fechaInicio: Date; operadorId: string }
| { estado: "enviado"; fechaEnvio: Date; numeroSeguimiento: string }
| { estado: "entregado"; fechaEntrega: Date; firma: string }
| { estado: "cancelado"; motivo: string };
function describir(pedido: EstadoPedido): string {
switch (pedido.estado) {
case "pendiente":
return `Pedido creado el ${pedido.fechaCreacion.toLocaleDateString()}`;
case "procesando":
return `En proceso desde el ${pedido.fechaInicio.toLocaleDateString()} por ${pedido.operadorId}`;
case "enviado":
return `Enviado el ${pedido.fechaEnvio.toLocaleDateString()}, seguimiento: ${pedido.numeroSeguimiento}`;
case "entregado":
return `Entregado el ${pedido.fechaEntrega.toLocaleDateString()}, firmado por: ${pedido.firma}`;
case "cancelado":
return `Cancelado: ${pedido.motivo}`;
}
}TypeScript verifica que el switch sea exhaustivo: si añades una nueva variante al tipo y olvidas manejarla, el compilador te lo advierte.
Verificación de exhaustividad explícita
Para casos donde usas if/else en lugar de switch, puedes forzar la verificación con el tipo never:
function verificarExhaustividad(valor: never): never {
throw new Error(`Caso no manejado: ${JSON.stringify(valor)}`);
}
function procesarEstado(pedido: EstadoPedido): void {
if (pedido.estado === "pendiente") {
// ...
} else if (pedido.estado === "procesando") {
// ...
} else {
// Si olvidas algún caso, TypeScript marca error aquí
verificarExhaustividad(pedido);
}
}Custom type guards con `is`
Un type guard personalizado es una función que devuelve un predicado de tipo (param is Tipo). Cuando la función retorna true, TypeScript sabe que param es del tipo indicado:
interface Usuario {
id: number;
nombre: string;
rol: "admin" | "usuario";
}
interface Invitado {
sessionId: string;
expira: Date;
}
type Visitante = Usuario | Invitado;
// Type guard personalizado
function esUsuario(visitante: Visitante): visitante is Usuario {
return "id" in visitante && "rol" in visitante;
}
function obtenerIdentificador(visitante: Visitante): string {
if (esUsuario(visitante)) {
// TypeScript sabe que es Usuario
return `Usuario #${visitante.id} (${visitante.nombre})`;
}
// TypeScript sabe que es Invitado
return `Invitado temporal: ${visitante.sessionId}`;
}Los type guards son especialmente útiles cuando la lógica de discriminación es compleja y quieres encapsularla y reutilizarla.
Assertion functions
Una assertion function lanza un error si la condición no se cumple y le indica a TypeScript que puede confiar en el tipo después de la llamada:
function assertEsString(valor: unknown): asserts valor is string {
if (typeof valor !== "string") {
throw new TypeError(`Se esperaba string, se recibió ${typeof valor}`);
}
}
function assertNoNulo<T>(
valor: T,
mensaje: string
): asserts valor is NonNullable<T> {
if (valor === null || valor === undefined) {
throw new Error(mensaje);
}
}
// Uso:
function procesarConfiguracion(config: unknown): void {
assertEsString(config);
// A partir de aquí TypeScript sabe que config es string
const partes = config.split(",");
console.log("Opciones:", partes);
}
function obtenerUsuarioActual(id: number): Usuario | null {
// Simulación de búsqueda
return id === 1 ? { id: 1, nombre: "Ana", rol: "admin" } : null;
}
function cargarPerfil(id: number): string {
const usuario = obtenerUsuarioActual(id);
assertNoNulo(usuario, `Usuario con id ${id} no encontrado`);
// Aquí TypeScript sabe que usuario no es null
return usuario.nombre;
}Ejemplo completo: manejador de respuesta API
// ──────────────────────────────────────────────────────────────
// Tipos de respuesta
// ──────────────────────────────────────────────────────────────
type RespuestaExitosa<T> = {
tipo: "exito";
datos: T;
timestamp: number;
};
type RespuestaError = {
tipo: "error";
mensaje: string;
codigo: number;
};
type RespuestaAPI<T> = RespuestaExitosa<T> | RespuestaError;
// ──────────────────────────────────────────────────────────────
// Type guard para distinguir variantes
// ──────────────────────────────────────────────────────────────
function esExitosa<T>(r: RespuestaAPI<T>): r is RespuestaExitosa<T> {
return r.tipo === "exito";
}
// ──────────────────────────────────────────────────────────────
// Procesador genérico
// ──────────────────────────────────────────────────────────────
function procesarRespuesta<T>(
respuesta: RespuestaAPI<T>,
onExito: (datos: T) => void,
onError: (codigo: number, mensaje: string) => void
): void {
if (esExitosa(respuesta)) {
onExito(respuesta.datos);
} else {
onError(respuesta.codigo, respuesta.mensaje);
}
}
// ──────────────────────────────────────────────────────────────
// Simulación de uso
// ──────────────────────────────────────────────────────────────
interface Articulo {
id: number;
titulo: string;
contenido: string;
}
const respuestaOk: RespuestaAPI<Articulo> = {
tipo: "exito",
datos: { id: 1, titulo: "Hola mundo", contenido: "Primer artículo" },
timestamp: Date.now(),
};
const respuestaFallo: RespuestaAPI<Articulo> = {
tipo: "error",
mensaje: "Recurso no encontrado",
codigo: 404,
};
procesarRespuesta(
respuestaOk,
(art) => console.log(`Artículo cargado: ${art.titulo}`),
(cod, msg) => console.error(`Fallo ${cod}: ${msg}`)
);
procesarRespuesta(
respuestaFallo,
(art) => console.log(art.titulo),
(cod, msg) => console.error(`Fallo ${cod}: ${msg}`)
);Resumen de técnicas de narrowing
| Técnica | Ejemplo | Mejor para |
|---|---|---|
typeof |
typeof x === "string" |
Tipos primitivos |
| Truthiness | if (valor) |
null, undefined, falsy |
| Equality | x === "opcion" |
Literales y enums |
in |
"propiedad" in obj |
Objetos con formas distintas |
instanceof |
x instanceof Clase |
Instancias de clases |
| Discriminated union | switch (x.tipo) |
Uniones con discriminador |
Type guard (is) |
función(x): x is T |
Lógica compleja reutilizable |
| Assertion function | asserts x is T |
Validaciones imperativas |
En la siguiente lección explorarás los utility types de TypeScript: herramientas listas para usar que transforman tipos existentes sin necesidad de reescribirlos desde cero.
Inicia sesión para guardar tu progreso