TypeScript atrapa la mayoría de los errores antes de ejecutar, pero no todos. A veces deja pasar código que falla, y a veces rechaza código que parece correcto. Estas son las rarezas de TypeScript más famosas, comprobadas con el compilador y ejecutadas de verdad, con la explicación de por qué pasan y la costumbre que las evita.
TypeScript promete atrapar los errores antes de ejecutar el programa, y casi siempre lo cumple. Pero tiene huecos, y no por descuido: muchos son decisiones conscientes de su equipo, que prefirió un lenguaje cómodo de usar con el JavaScript que ya existe antes que uno perfecto pero imposible de adoptar. Aquí están los más llamativos. Cada fragmento se comprobó con TypeScript 7.0.2 en modo estricto (tsc --strict, con los mensajes en español) y, cuando hace falta ver lo que pasa al ejecutarlo, con Node.js 24.
- any lo contagia todo
- as le da la razón a quien escribe
- Las propiedades de más solo se vigilan a veces
- Una unión de objetos acepta los dos a la vez
- El tipo {} acepta casi cualquier cosa
- Un array de perros puede acabar sin perros
- Los métodos son más permisivos que las funciones
- Object.keys devuelve string[]
- Para TypeScript, un array nunca se sale de rango
- El estrechamiento se olvida dentro de las funciones
- readonly solo protege el primer nivel
- Una función void puede devolver algo
- enum: el tipo que sí existe al ejecutar
- Las interfaces no existen al ejecutar
.ts directamente: Node 24 borra los tipos y ejecuta el resto sin revisarlos. Por eso un programa que compila «limpio» puede fallar, y uno con errores de tipo puede ejecutarse. Revisar los tipos es trabajo de tsc.any lo contagia todo
const datos: any = JSON.parse('{"precio": "12.50"}');
const total: number = datos.precio + 1;
console.log(total);
console.log(total.toFixed(2));Ábrelo en el editor y cámbialo →
tsc --strictSin errores.
node archivo.ts12.501
y despuésTypeError: total.toFixed is not a function
Por qué: JSON.parse devuelve any, y any significa «no revises nada». Todo lo que se saca de un any también es any: datos.precio + 1 es any, y un any se puede asignar a una variable de tipo number sin ninguna queja. El precio venía como texto, así que la suma concatenó: total vale "12.501" aunque su tipo diga number, y toFixed no existe. Un solo any desactiva la revisión en todo el código por el que pasa.
unknown y compruébalo antes de usarlo, a mano o con una biblioteca de validación como Zod. unknown acepta cualquier valor, pero no deja hacer nada con él hasta comprobar su tipo. Y vigila los any que no escribes tú: JSON.parse, response.json() o una biblioteca sin tipos.as le da la razón a quien escribe
const usuario = JSON.parse('{"nombre": "Ana"}') as { nombre: string; edad: number };
console.log(usuario.nombre);
console.log(usuario.edad.toFixed(0));Ábrelo en el editor y cámbialo →
tsc --strictSin errores.
node archivo.tsAna
y despuésTypeError: Cannot read properties of undefined (reading 'toFixed')
Por qué: as no convierte nada ni comprueba nada al ejecutar: le dice al compilador «confía en mí, esto es de este tipo». El compilador confía, y si te equivocas, el error aparece donde menos lo esperas. Solo protesta cuando la afirmación es imposible, cuando los dos tipos no tienen nada que ver:
const a = "hola" as number; const b = "hola" as unknown as number;
Ábrelo en el editor y cámbialo →
tsc --strictlínea 1: error TS2352: La conversión del tipo "string" al tipo "number" puede ser un error, porque ninguno de los tipos se superpone suficientemente al otro. Si esto era intencionado, convierta primero la expresión en "unknown".
Fíjate en que el propio mensaje te dice cómo saltarte la protección: con as unknown as number, la segunda línea compila sin rechistar. Es la puerta trasera del sistema de tipos.
as solo cuando sabes algo que el compilador no puede saber, y nunca para «callar» un error. Si el dato viene de fuera, valídalo; si es tuyo, casi siempre basta con anotar bien el tipo. Una doble conversión as unknown as en una revisión de código merece siempre una pregunta.Las propiedades de más solo se vigilan a veces
interface Opciones {
color?: string;
ancho?: number;
}
function dibujar(o: Opciones) {
console.log(o.color ?? "negro", o.ancho ?? 1);
}
dibujar({ colr: "rojo" });Ábrelo en el editor y cámbialo →
tsc --strictlínea 8: error TS2561: El literal de objeto solo puede especificar propiedades conocidas, pero "colr" no existe en el tipo "Opciones". ¿Quería escribir "color"?
Bien: TypeScript detecta la errata y hasta sugiere la palabra correcta. Ahora, el mismo objeto guardado antes en una variable:
interface Opciones {
color?: string;
ancho?: number;
}
function dibujar(o: Opciones) {
console.log(o.color ?? "negro", o.ancho ?? 1);
}
const misOpciones = { colr: "rojo", ancho: 2 };
dibujar(misOpciones);Ábrelo en el editor y cámbialo →
tsc --strictSin errores.
node archivo.tsnegro 2
Por qué: TypeScript es estructural: un valor sirve para un tipo si tiene lo que el tipo pide, aunque traiga más cosas. misOpciones tiene un ancho numérico, y como color es opcional, encaja en Opciones. La comprobación de «propiedades de más» es una regla extra que solo se aplica a los objetos escritos en el mismo sitio donde se usan, porque ahí una propiedad que sobra no puede servir para nada más y casi seguro es un error. El color pedido se pierde sin aviso: se dibuja en negro.
Hay una excepción que salva algunos casos: si la variable no tiene ninguna propiedad en común con un tipo cuyas propiedades son todas opcionales (un «tipo débil»), TypeScript sí protesta. Con const misOpciones = { colr: "rojo" }, sin el ancho:
tsc --strictlínea 9: error TS2559: El tipo "{ colr: string; }" no tiene propiedades en común con el tipo "Opciones".const misOpciones: Opciones = { colr: "rojo", ancho: 2 } sí da el error, porque el literal se comprueba contra el tipo en el momento de escribirlo.Una unión de objetos acepta los dos a la vez
type Contacto = { correo: string } | { telefono: string };
const c: Contacto = { correo: "ana@ejemplo.com", telefono: "555-0100" };
console.log(c);Ábrelo en el editor y cámbialo →
tsc --strictSin errores.
node archivo.ts{ correo: 'ana@ejemplo.com', telefono: '555-0100' }Por qué: una unión A | B significa «vale si encaja en A o en B», no «en uno y solo uno». El objeto encaja en { correo: string } (le sobra telefono, pero la otra rama de la unión la conoce, así que no cuenta como propiedad de más) y también en { telefono: string }. Si querías decir «o correo o teléfono, pero no los dos», este tipo no lo dice.
La solución es una unión discriminada: una propiedad común con un valor literal distinto en cada rama. Con ella, TypeScript sabe en qué rama estás y vuelve a vigilar las propiedades de más:
type Contacto =
| { tipo: "correo"; correo: string }
| { tipo: "telefono"; telefono: string };
const c: Contacto = { tipo: "correo", correo: "ana@ejemplo.com", telefono: "555-0100" };Ábrelo en el editor y cámbialo →
tsc --strictlínea 4: error TS2353: El literal de objeto solo puede especificar propiedades conocidas y 'telefono' no existe en el tipo '{ tipo: "correo"; correo: string; }'.tipo, kind, estado…). Además del error de arriba, te da el estrechamiento automático en los switch, como se explica en la lección de uniones y estrechamiento.El tipo {} acepta casi cualquier cosa
let cualquiera: {} = 42;
cualquiera = "hola";
cualquiera = [1, 2, 3];
cualquiera = () => "función";
cualquiera = null;Ábrelo en el editor y cámbialo →
tsc --strictlínea 5: error TS2322: El tipo 'null' no se puede asignar al tipo '{}'.Por qué: {} parece «un objeto vacío», pero como tipo significa «cualquier valor al que se le pueda pedir una propiedad»: todo menos null y undefined. Un número, un texto, un array y una función tienen propiedades (toFixed, length…), así que las cuatro primeras asignaciones compilan. Solo falla la última. Es el tipo estructural más amplio que existe: no pide ninguna propiedad, así que casi todo lo cumple.
unknown; para «cualquier objeto» (no primitivos), object; para un objeto con claves de texto, Record<string, unknown>. El tipo {} solo tiene sentido en trucos muy concretos, como NonNullable.Un array de perros puede acabar sin perros
class Animal {
nombre = "animal";
}
class Perro extends Animal {
ladrar() {
return "guau";
}
}
const perros: Perro[] = [new Perro()];
const animales: Animal[] = perros;
animales.push(new Animal());
console.log(perros.length);
console.log(perros[1].ladrar());Ábrelo en el editor y cámbialo →
tsc --strictSin errores.
node archivo.ts2
y despuésTypeError: perros[1].ladrar is not a function
Por qué: TypeScript deja usar un Perro[] donde se pide un Animal[], porque leer de él es seguro: todo perro es un animal. Pero escribir no lo es, y el compilador no distingue lo uno de lo otro. perros y animales son el mismo array con dos nombres; al meter un Animal por uno, el otro sigue creyendo que solo tiene perros. Es un hueco conocido y aceptado: cerrarlo obligaría a marcar como error muchísimo código correcto que solo lee los arrays.
readonly Animal[]: así le puedes pasar un Perro[] sin miedo, porque no tiene push. Y desconfía de cualquier función que reciba un array de un tipo general y le añada elementos.Los métodos son más permisivos que las funciones
interface ConMetodo {
procesar(x: string | number): void;
}
interface ConPropiedad {
procesar: (x: string | number) => void;
}
const soloTexto = {
procesar(x: string) {
console.log(x.toUpperCase());
},
};
const a: ConMetodo = soloTexto;
const b: ConPropiedad = soloTexto;
a.procesar(42);Ábrelo en el editor y cámbialo →
tsc --strictlínea 13: error TS2322: El tipo '{ procesar(x: string): void; }' no se puede asignar al tipo 'ConPropiedad'.
Los tipos de propiedad 'procesar' no son compatibles.
El tipo '(x: string) => void' no se puede asignar al tipo '(x: string | number) => void'.
Los tipos de parámetros 'x' y 'x' no son compatibles.
El tipo 'string | number' no se puede asignar al tipo 'string'.
El tipo 'number' no se puede asignar al tipo 'string'.Solo hay un error, y no es el que cabría esperar. ConMetodo y ConPropiedad piden lo mismo: algo que sepa procesar textos y números. soloTexto solo sabe procesar textos. TypeScript lo rechaza cuando el tipo está escrito como propiedad con una función (procesar: (x) => void), pero lo acepta cuando está escrito como método (procesar(x): void). Y lo que se acepta, falla:
interface ConMetodo {
procesar(x: string | number): void;
}
const soloTexto = {
procesar(x: string) {
console.log(x.toUpperCase());
},
};
const a: ConMetodo = soloTexto;
a.procesar("hola");
a.procesar(42);Ábrelo en el editor y cámbialo →
node archivo.tsHOLA
y despuésTypeError: x.toUpperCase is not a function
Por qué: con la opción strictFunctionTypes (incluida en strict), los parámetros de las funciones se comprueban bien: una función que solo acepta string no sirve donde se pueden recibir números. Pero los métodos quedaron fuera a propósito, porque la biblioteca estándar y el DOM están llenos de métodos declarados así, y comprobarlos con rigor rompería código que funciona.
procesar: (x: string | number) => void, y tendrás la comprobación estricta. Hay una regla de ESLint que lo exige: @typescript-eslint/method-signature-style.Object.keys devuelve string[]
const precios = { cafe: 30, te: 25 };
for (const clave of Object.keys(precios)) {
console.log(clave, precios[clave]);
}Ábrelo en el editor y cámbialo →
tsc --strictlínea 3: error TS7053: El elemento tiene un tipo "any" de forma implícita porque la expresión de tipo "string" no se puede usar para indexar el tipo "{ cafe: number; te: number; }".
No se encontró ninguna signatura de índice con un parámetro de tipo "string" en el tipo "{ cafe: number; te: number; }".Por qué: Object.keys devuelve string[], no ("cafe" | "te")[], y parece un descuido. No lo es: por el tipado estructural, un objeto puede tener más propiedades de las que dice su tipo, y Object.keys las devuelve todas. Este ejemplo lo demuestra forzando el tipo de la clave con as:
interface Precios {
cafe: number;
te: number;
}
function sumar(p: Precios) {
let total = 0;
for (const clave of Object.keys(p)) {
total += p[clave as keyof Precios];
}
return total;
}
const conMas = { cafe: 30, te: 25, nota: "precios de octubre" };
console.log(sumar(conMas));Ábrelo en el editor y cámbialo →
tsc --strictSin errores.
node archivo.ts55precios de octubre
Al sumar las propiedades, la tercera, nota, que el tipo no menciona, se coló como texto: 30 + 25 son 55, y después se concatenó la nota. Si TypeScript prometiera que las claves son solo "cafe" | "te", estaría mintiendo.
Object.entries(precios). Si de verdad controlas el objeto (lo acabas de crear tú), puedes usar Object.keys(precios) as (keyof typeof precios)[], sabiendo que es una promesa tuya. Y para diccionarios con claves dinámicas, mejor un Map o un Record<string, number>.Para TypeScript, un array nunca se sale de rango
const nombres = ["Ana", "Luis"]; const tercero = nombres[2]; console.log(tercero.toUpperCase());
Ábrelo en el editor y cámbialo →
tsc --strictSin errores.
node archivo.tsTypeError: Cannot read properties of undefined (reading 'toUpperCase')
Por qué: el tipo de nombres[2] es string, sin más. TypeScript no sabe cuántos elementos tendrá un array al ejecutar, y si cada acceso por índice devolviera string | undefined, habría que comprobar hasta lista[i] dentro de un for que ya controla el límite. Así que, por defecto, supone que el índice existe. Lo mismo pasa con los diccionarios del tipo Record<string, ...>. Existe una opción, fuera de strict, que cambia la regla:
const nombres = ["Ana", "Luis"]; const tercero = nombres[2]; console.log(tercero.toUpperCase());
tsc --strictlínea 3: error TS18048: "tercero" es posiblemente "undefined".
Ese mensaje sale al activar noUncheckedIndexedAccess. Con ella, la línea 3 da string | undefined y hay que comprobarla antes de usarla.
"noUncheckedIndexedAccess": true a tu tsconfig.json en los proyectos nuevos; obliga a comprobar, pero es exactamente la comprobación que evita este fallo. Para recorrer arrays, for...of no tiene el problema: nunca da un elemento que no existe.El estrechamiento se olvida dentro de las funciones
const estado: { mensaje: string | null } = { mensaje: "hola" };
if (estado.mensaje !== null) {
console.log(estado.mensaje.length);
setTimeout(() => {
console.log(estado.mensaje.length);
}, 0);
}Ábrelo en el editor y cámbialo →
tsc --strictlínea 5: error TS18047: "estado.mensaje" es posiblemente "null".
La línea 3 compila y la 5, idéntica, no. Por qué: después del if, TypeScript sabe que estado.mensaje no es null… en ese momento. Pero la función de setTimeout se ejecuta más tarde, y para entonces cualquier otro código pudo cambiar estado.mensaje a null. Como el compilador no puede saber cuándo se ejecutará una función, en las propiedades de objetos (y en las variables con let que se reasignan después) descarta el estrechamiento al entrar en ella. Tiene razón en desconfiar.
La solución es copiar el valor a una const: una constante no puede cambiar, así que el estrechamiento sobrevive dentro de la función, aunque el objeto cambie después:
const estado: { mensaje: string | null } = { mensaje: "hola" };
const mensaje = estado.mensaje;
if (mensaje !== null) {
setTimeout(() => {
console.log(mensaje.length);
}, 0);
}
estado.mensaje = null;Ábrelo en el editor y cámbialo →
tsc --strictSin errores.
node archivo.ts4
const lo que ya comprobaste. Además del error, te ahorras un fallo real: el de leer un valor que cambió mientras esperabas.readonly solo protege el primer nivel
const config: Readonly<{ servidor: { puerto: number } }> = {
servidor: { puerto: 80 },
};
config.servidor.puerto = 8080;
console.log(config.servidor.puerto);Ábrelo en el editor y cámbialo →
tsc --strictSin errores.
node archivo.ts8080
Por qué: Readonly<T> (y la palabra readonly) solo protege las propiedades del primer nivel. Reemplazar el servidor entero sí da error:
const config: Readonly<{ servidor: { puerto: number } }> = {
servidor: { puerto: 80 },
};
config.servidor = { puerto: 8080 };Ábrelo en el editor y cámbialo →
tsc --strictlínea 4: error TS2540: No se puede asignar a "servidor" porque es una propiedad de solo lectura.
Pero cambiar una propiedad del objeto de dentro, no. Además, readonly solo existe al compilar: el JavaScript resultante no protege nada, igual que const en JavaScript impide reasignar la variable pero no modificar el objeto.
as const los hace de solo lectura en todos los niveles. Para estructuras recibidas, un tipo recursivo como type Profundo<T> = { readonly [K in keyof T]: Profundo<T[K]> }. Y si necesitas la protección también al ejecutar, Object.freeze, que tampoco es profundo.Una función void puede devolver algo
type Avisar = () => void; const avisar: Avisar = () => 42; const r = avisar(); console.log(r);
Ábrelo en el editor y cámbialo →
tsc --strictSin errores.
node archivo.ts42
Por qué: en un tipo de función, void no significa «no devuelve nada», sino «lo que devuelva, no lo voy a usar». Es lo que permite escribir lista.forEach((n) => otra.push(n)): push devuelve un número, pero forEach espera una función void y la acepta igual. El valor sigue ahí: r tiene tipo void y vale 42. En cambio, en una declaración de función, void sí es una promesa:
function avisar(): void {
return 42;
}Ábrelo en el editor y cámbialo →
tsc --strictlínea 2: error TS2322: El tipo 'number' no se puede asignar al tipo 'void'.
void: para TypeScript no existe, pero al ejecutar puede ser cualquier cosa. Si necesitas que una función no devuelva nada, escríbelo en su declaración, no solo en el tipo de una variable.enum: el tipo que sí existe al ejecutar
enum Estado {
Activo,
Pausado,
}
console.log(Estado);
const n: number = 42;
const e: Estado = n;
console.log(e);Ábrelo en el editor y cámbialo →
tsc --strictSin errores.
node archivo.tsSyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
Por qué: casi todo TypeScript son tipos que se borran al compilar: quitas las anotaciones y queda JavaScript válido. enum es la gran excepción, porque genera código. Node 24 ejecuta TypeScript precisamente borrando los tipos, sin compilar, y un enum no se puede borrar: Node se niega. Compilado con tsc, esto es lo que se ejecuta:
var Estado;
(function (Estado) {
Estado[Estado["Activo"] = 0] = "Activo";
Estado[Estado["Pausado"] = 1] = "Pausado";
})(Estado || (Estado = {}));
console.log(Estado);
const n = 42;
const e = n;
console.log(e);node (el .js compilado){ '0': 'Activo', '1': 'Pausado', Activo: 0, Pausado: 1 }
42Dos sorpresas más. El objeto tiene las claves en los dos sentidos (Estado.Activo es 0 y Estado[0] es "Activo"), así que Object.keys(Estado) devuelve cuatro claves, no dos. Y un enum numérico acepta cualquier número que llegue en una variable de tipo number: e vale 42, que no es ningún estado. Solo un literal fuera de rango, como const e: Estado = 42, da error.
type Estado = "activo" | "pausado". Si necesitas también la lista de valores al ejecutar, un objeto as const. Hay una opción, erasableSyntaxOnly, que marca como error todo lo que Node no puede borrar: enum, namespace con código y los parámetros con public en los constructores.Las interfaces no existen al ejecutar
interface Usuario {
nombre: string;
}
function esUsuario(x: unknown) {
return x instanceof Usuario;
}Ábrelo en el editor y cámbialo →
tsc --strictlínea 5: error TS2693: 'Usuario' solo hace referencia a un tipo, pero aquí se usa como valor.
Por qué: instanceof se ejecuta en JavaScript, y una interface no existe en JavaScript: se borra al compilar. Al ejecutar no hay nada con lo que comparar. Pasa lo mismo con cualquier tipo: no se puede preguntar «¿esto es un Usuario?» en tiempo de ejecución. Hay que preguntar por lo que tiene, y una guarda de tipo (x is Usuario) le transmite la respuesta al compilador:
interface Usuario {
nombre: string;
}
function esUsuario(x: unknown): x is Usuario {
return typeof x === "object" && x !== null && "nombre" in x && typeof x.nombre === "string";
}
console.log(esUsuario({ nombre: "Ana" }), esUsuario({ nombre: 7 }), esUsuario(null));Ábrelo en el editor y cámbialo →
tsc --strictSin errores.
node archivo.tstrue false false
instanceof, usa una class, que sí existe al ejecutar. Para datos que llegan como JSON (que nunca son instancias de nada), una guarda de tipo como la de arriba o un validador. Ojo: TypeScript no comprueba que la guarda diga la verdad; si su código está mal, el error vuelve a ser tuyo.Resumen: la regla detrás de cada rareza
| Rareza | La causa | La costumbre que la evita |
|---|---|---|
any, as | Desactivan la revisión a petición tuya | unknown y validar lo que llega de fuera |
| Propiedades de más, uniones | Tipado estructural: sobrar no es un error | Literales en el sitio y uniones discriminadas |
{} | Un tipo que no pide nada lo cumple todo | unknown u object |
| Arrays, métodos | Huecos aceptados para no romper código correcto | readonly en parámetros y funciones como propiedades |
Object.keys | Un objeto puede tener más de lo que dice su tipo | Object.entries |
| Índices | El compilador no sabe la longitud de un array | noUncheckedIndexedAccess |
| Estrechamiento en callbacks | La función se ejecuta más tarde | Copiar a una const |
readonly | Solo protege el primer nivel | as const |
void | En un tipo de función significa «no lo uso» | No usar nunca ese valor |
enum, interfaces | Unos tipos generan código y otros se borran | Uniones de literales, guardas de tipo |
¿Quieres comprobar si ya piensas como el compilador? Prueba el test «¿compila o no?», o abre cualquiera de estos fragmentos en el editor de TypeScript y cámbialo hasta entender por qué compila o deja de hacerlo.
Todo el código de esta página se comprobó con TypeScript 7.0.2 (tsc --strict --locale es) y se ejecutó con Node.js 24.22 en un contenedor limpio; los mensajes del compilador y las salidas están copiados de esa ejecución.
