Una función que lanza una excepción no lo dice en su tipo: leerEdad(texto: string): number puede fallar y nada en la firma lo avisa. Una alternativa para los errores, muy usada en Rust y en Go, es devolverlos como un valor más. Con una unión discriminada, TypeScript obliga a mirar si salió bien antes de usar el resultado.
También te puede interesar
En un catch, el error es unknown
En JavaScript se puede lanzar cualquier cosa: un Error, un texto, un número. Por eso, en modo estricto, lo que llega al catch es unknown:
// En un catch, TypeScript no sabe qué se lanzó: en modo estricto, el error es unknown.
function leerEdad(texto: string): number {
const n = Number(texto);
if (!Number.isInteger(n) || n < 0) throw new Error(`«${texto}» no es una edad válida`);
return n;
}
try {
leerEdad("veinte");
} catch (e) {
console.log(e.message);
}Ábrelo en el editor y cámbialo →
npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es catch.ts
tsccatch.ts(11,15): error TS18046: "e" es de tipo "unknown".
Hay que comprobar qué es antes de usarlo:
function leerEdad(texto: string): number {
const n = Number(texto);
if (!Number.isInteger(n) || n < 0) throw new Error(`«${texto}» no es una edad válida`);
return n;
}
try {
leerEdad("veinte");
} catch (e) {
// Antes de usarlo hay que comprobar qué es: alguien pudo hacer throw "texto" o throw 42.
console.log(e instanceof Error ? e.message : String(e));
}Ábrelo en el editor y cámbialo →
node catch-bien.ts
salida«veinte» no es una edad válida
El tipo Result
Un Resultado es una unión de dos variantes: un éxito con su valor o un fallo con su error. La propiedad ok las distingue. Los errores, en vez de mensajes sueltos, son una unión de nombres conocidos:
// Un resultado es un éxito con su valor o un fallo con su error. Nunca las dos cosas.
type Resultado<T, E = string> =
| { ok: true; valor: T }
| { ok: false; error: E };
const exito = <T>(valor: T): Resultado<T, never> => ({ ok: true, valor });
const fallo = <E>(error: E): Resultado<never, E> => ({ ok: false, error });
type ErrorEdad = "vacia" | "no-numerica" | "fuera-de-rango";
function leerEdad(texto: string): Resultado<number, ErrorEdad> {
if (texto.trim() === "") return fallo("vacia");
const n = Number(texto);
if (!Number.isInteger(n)) return fallo("no-numerica");
if (n < 0 || n > 130) return fallo("fuera-de-rango");
return exito(n);
}
const mensajes: Record<ErrorEdad, string> = {
"vacia": "Escribe tu edad",
"no-numerica": "La edad tiene que ser un número entero",
"fuera-de-rango": "Esa edad no parece real",
};
for (const entrada of ["34", "", "veinte", "200"]) {
const r = leerEdad(entrada);
if (r.ok) {
console.log(`«${entrada}» -> edad ${r.valor}`);
} else {
console.log(`«${entrada}» -> ${mensajes[r.error]}`);
}
}Ábrelo en el editor y cámbialo →
npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es resultado.ts
node resultado.ts
salida«34» -> edad 34 «» -> Escribe tu edad «veinte» -> La edad tiene que ser un número entero «200» -> Esa edad no parece real
Ahora la firma lo dice todo: leerEdad devuelve un número o uno de tres errores concretos. Y gracias al Record<ErrorEdad, string>, si alguien añade un cuarto error, el objeto de mensajes deja de compilar hasta que le dé su texto.
No se puede olvidar el error
Con una excepción, nada obliga a poner un try. Con un Resultado, usar el valor sin mirar ok no compila:
type Resultado<T, E = string> = { ok: true; valor: T } | { ok: false; error: E };
declare function leerEdad(texto: string): Resultado<number, "vacia" | "no-numerica">;
const r = leerEdad("34");
console.log(r.valor + 1); // se nos olvidó mirar r.oknpx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es olvido.ts
tscolvido.ts(6,15): error TS2339: La propiedad 'valor' no existe en el tipo 'Resultado<number, "no-numerica" | "vacia">'.
La propiedad 'valor' no existe en el tipo '{ ok: false; error: "no-numerica" | "vacia"; }'.Encadenar pasos
Con una función que solo continúa si el paso anterior salió bien, se encadenan varias operaciones. El primer error que aparezca pasa hasta el final sin ejecutar el resto:
type Resultado<T, E = string> = { ok: true; valor: T } | { ok: false; error: E };
const exito = <T>(valor: T): Resultado<T, never> => ({ ok: true, valor });
const fallo = <E>(error: E): Resultado<never, E> => ({ ok: false, error });
// Aplica f solo si el paso anterior salió bien; si no, deja pasar el error tal cual.
function luego<T, U, E>(r: Resultado<T, E>, f: (v: T) => Resultado<U, E>): Resultado<U, E> {
return r.ok ? f(r.valor) : r;
}
const aNumero = (s: string): Resultado<number> =>
Number.isFinite(Number(s)) && s.trim() !== "" ? exito(Number(s)) : fallo(`«${s}» no es un número`);
const positivo = (n: number): Resultado<number> => (n > 0 ? exito(n) : fallo(`${n} no es positivo`));
const pesos = new Intl.NumberFormat("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const conIva = (n: number): Resultado<string> => exito(`$${pesos.format(n * 1.16)} MXN`);
for (const s of ["1000", "-5", "mil"]) {
const r = luego(luego(aNumero(s), positivo), conIva);
console.log(s, "->", r.ok ? r.valor : `error: ${r.error}`);
}Ábrelo en el editor y cámbialo →
npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es encadenar.ts
node encadenar.ts
salida1000 -> $1,160.00 MXN -5 -> error: -5 no es positivo mil -> error: «mil» no es un número
| Lanzar excepciones | Devolver un Resultado | |
|---|---|---|
| La firma avisa de que puede fallar | No | Sí |
| El compilador obliga a tratar el error | No | Sí |
| Tipo del error | unknown en el catch | El que declares |
| Bueno para | Fallos inesperados: sin memoria, un error de programación | Fallos esperados: datos inválidos, algo que no se encontró |
Resultado para los errores que forman parte del funcionamiento normal, como una entrada inválida, y excepciones para lo que de verdad no debería pasar.Todo el código de esta página se comprobó con TypeScript 7.0.2 en modo estricto (--strict --locale es) y se ejecutó con Node.js 24.22 en un contenedor limpio antes de publicarla; los mensajes de error y las salidas están copiados de esa ejecución. Documentación oficial: useUnknownInCatchVariables en la referencia de TSConfig.