Llevo casi dos años usando TypeScript 5.x activamente — primero en un proyecto personal, luego en producción con un equipo de seis personas construyendo una plataforma SaaS B2B. No he probado cada feature de cada minor release, pero sí he ido adoptando las que tenían sentido para nuestro stack: Next.js en el frontend, NestJS en la API principal, un par de workers con Bun corriendo tareas de procesamiento en background.
Este artículo no es una lista de todo lo que salió. Es sobre lo que realmente usé, lo que me funcionó, y honestamente, lo que me decepcionó un poco.
Decoradores Estables: La Historia de Nunca Acabar que Por Fin Acabó
Si llevas tiempo en el ecosistema TypeScript, sabes que los decoradores experimentales de experimentalDecorators: true estuvieron rondando durante demasiado tiempo. TypeScript 5.0, lanzado en marzo de 2023, finalmente implementó el estándar ECMAScript de decoradores. No son los mismos decoradores de antes. Son mejores, pero implican una migración real que nadie te avisa que va a doler un poco.
Migramos nuestros controladores de NestJS a los nuevos decoradores durante el primer trimestre de 2024. Fue más suave de lo que esperaba — la mayoría de librerías ya habían añadido soporte — pero encontré un par de casos raros con decoradores en propiedades de clase donde el comportamiento difería del sistema legacy. Uno de esos casos me tuvo depurando durante una tarde entera porque el error en runtime no mencionaba los decoradores para nada.
// Decorador de clase con el nuevo estándar ECMAScript
function singleton<T extends { new(...args: any[]): {} }>(
target: T,
context: ClassDecoratorContext
) {
let instance: InstanceType<T> | undefined;
return class extends target {
constructor(...args: any[]) {
if (instance) return instance;
super(...args);
instance = this as unknown as InstanceType<T>;
}
} as T;
}
@singleton
class DatabasePool {
connection = createConnection();
}
El nuevo parámetro context es lo que más me gustó. Tienes acceso al nombre, al tipo del decorador, y a addInitializer para ejecutar lógica post-construcción. Es mucho más explícito que el sistema anterior, donde básicamente estabas adivinando el orden de ejecución en algunos casos edge.
Lo que me decepcionó: no hay compatibilidad automática con el código legacy. Si tienes decoradores propios escritos para el sistema experimental, los tienes que reescribir desde cero. En nuestro caso fueron cuatro decoradores internos — un par de horas de trabajo — pero conozco equipos con docenas de decoradores propios que tardaron semanas en la migración completa. Haz el inventario de decoradores propios antes de comprometerte con una fecha. Y no hagas la migración el viernes por la tarde. Yo aprendí eso de la manera difícil.
const en Genéricos y NoInfer: Dos Cambios Pequeños, Mucho Menos Ruido
¿Cuántas veces terminaste con un tipo más ancho de lo que querías, el código compiló, los tests pasaron, y seis semanas después alguien pasó un valor inválido que el tipo debería haber rechazado? Eso es exactamente lo que resuelven estos dos — const type parameters en 5.0 y NoInfer en 5.4 — aunque llegaron en versiones distintas.
const en genéricos es simple pero resuelve algo que antes requería un as const en cada callsite:
// Sin const: T se infiere como string[]
function createRoute<T extends string[]>(paths: T): T {
return paths;
}
const routes = createRoute(['/', '/about', '/contact']);
// tipo inferido: string[] — demasiado amplio
// Con const: T se infiere como el tuple literal exacto
function createRoute<const T extends string[]>(paths: T): T {
return paths;
}
const routes = createRoute(['/', '/about', '/contact']);
// tipo inferido: readonly ['/', '/about', '/contact'] — perfecto
Llevaba años escribiendo as const en cada callsite de funciones similares. Esto lo elimina. En nuestro sistema de configuración de rutas, donde necesitamos los tipos literales para generar breadcrumbs y validaciones, fue un cambio de calidad de vida enorme.
NoInfer es más sutil — tardé tiempo en ver cuándo usarlo, pensé que era un caso edge raro, y resulta que no. El caso principal: tienes un genérico T que se infiere desde un argumento, pero no quieres que otro argumento influya en esa inferencia y la amplíe inadvertidamente.
function setDefault<T>(
values: T[],
defaultValue: NoInfer<T>
): T[] {
return values.length ? values : [defaultValue];
}
setDefault(['a', 'b'], 'c'); // OK
setDefault(['a', 'b'], 42); // Error: number no es asignable a string
Antes de NoInfer, teníamos que recurrir a técnicas como T & {} o reestructurar la firma entera. Uno de los desarrolladores del equipo lo usó en una función de validación de feature flags y cerró dos bugs de runtime que llevaban meses en el backlog. Los bugs existían porque TypeScript aceptaba valores inválidos por cómo estaban estructuradas las firmas genéricas. Pequeño cambio, impacto real.
using para la Gestión de Recursos: Más Útil de lo que Pensé
TypeScript 5.2 implementó using y await using, basados en la propuesta de Explicit Resource Management de TC39. Cuando lo vi por primera vez pensé "interesante, pero cuándo lo uso realmente en mi día a día". La respuesta: más seguido de lo que esperaba.
Funciona así: cualquier objeto que implemente Symbol.dispose (o Symbol.asyncDispose) se limpia automáticamente al salir del scope. Como el using de C# o los context managers de Python:
class DatabaseTransaction {
private committed = false;
constructor(private db: Database) {}
commit() {
this.db.commit();
this.committed = true;
}
[Symbol.dispose]() {
if (!this.committed) {
this.db.rollback();
}
}
}
async function transferFunds(from: string, to: string, amount: number) {
using transaction = new DatabaseTransaction(db);
await db.debit(from, amount);
await db.credit(to, amount);
transaction.commit();
// Si algo lanza antes del commit, el rollback ocurre automáticamente al salir del scope
}
Lo que me sorprendió genuinamente fue empezar a ver cuántos lugares en nuestra codebase teníamos bloques try/finally para cleanup que se podían simplificar con using. Conexiones a caches temporales, file handles en workers de procesamiento de CSV, clients de APIs externas que necesitan un .close() explícito. No es que el código fuera incorrecto antes — pero era más verboso y, lo que es peor, más fácil de olvidar el cleanup en paths de error que nadie testea.
En un proyecto de frontend puro probablemente no lo uses mucho. Pero si tienes workers, scripts de migración de datos, o código de servidor con gestión explícita de recursos, vale la pena adoptarlo.
isolatedDeclarations en Monorepos: El Antes y el Después de Nuestros Builds
Esta es, para nuestro equipo específico, la funcionalidad más impactante de toda la serie 5.x. Llegó en TypeScript 5.5 y resolvió un problema que yo ni sabía que tenía nombre.
El problema: en un monorepo con múltiples paquetes TypeScript, el compilador necesita procesar todos los archivos de un paquete para generar sus .d.ts de declaraciones de tipos. Esto hace que el build sea inherentemente secuencial en ciertos puntos — un paquete no puede emitir sus tipos hasta que termina de compilar completamente, y los paquetes que dependen de él tienen que esperar.
isolatedDeclarations: true en el tsconfig añade una restricción: todas las exportaciones públicas deben tener tipos explícitos anotados, no pueden depender solo de inferencia. A cambio, herramientas como tsc, esbuild y otras pueden generar los .d.ts en paralelo sin necesitar procesar los archivos de dependencias primero.
Nuestro monorepo tiene doce paquetes. Con el setup anterior, el build completo en CI — incluyendo generación de tipos — tardaba entre 3 minutos 45 segundos y 4 minutos 10 segundos dependiendo del runner. Después de habilitar isolatedDeclarations y ajustar nuestro pipeline de Turborepo para aprovechar la paralelización, bajamos a 2 minutos 20 segundos de forma consistente. No es lineal con el número de paquetes — el beneficio depende mucho de la topología de dependencias — pero el impacto en nuestro caso fue muy tangible.
El coste es real: tienes que añadir anotaciones de tipo explícitas donde antes te apoyabas en inferencia para las exportaciones públicas. Nuestro linter marca automáticamente las violaciones con una regla propia, así que el burden en el día a día no es grande. El setup inicial requirió medio día de limpieza. Si tienes un monorepo TypeScript y no estás mirando esto, empieza por aquí antes de mirar cualquier otra optimización de build.
Predicados de Tipo Inferidos: El Bug que Llevaba Tres Meses en el Radar
TypeScript 5.5 también trajo algo que parece menor pero que tiene una elegancia conceptual que me gusta: el compilador ahora puede inferir que una función es un type predicate sin que tú lo declares explícitamente, siempre que la lógica sea clara.
Antes, si querías narrowing automático en el callsite, tenías que anotar el return type manualmente:
// Antes de 5.5: anotación explícita obligatoria para que funcione el narrowing
function isString(value: unknown): value is string {
return typeof value === 'string';
}
// TypeScript 5.5+: inferencia automática del type predicate
const isDefinedString = (v: string | undefined) => v !== undefined && v.length > 0;
const items = ['hello', undefined, 'world', undefined, ''];
const definedItems = items.filter(isDefinedString);
// definedItems ahora es string[], no (string | undefined)[]
// antes necesitabas el as string[] o anotar isDefinedString explícitamente
Tenía un bug — o más bien, un // @ts-ignore vergonzoso — en nuestro pipeline de procesamiento de eventos donde hacíamos .filter(Boolean) y luego teníamos que castear manualmente porque TS no infería el narrowing. Era de esos parches que vives con durante semanas hasta que te molesta lo suficiente como para investigarlo de verdad. Cuando actualicé a 5.5, desapareció solo. No tuve que tocar el código.
Una advertencia: si tienes predicados con lógica muy compleja, el compilador puede no inferirlo automáticamente y necesitarás la anotación explícita. Para los casos comunes de filtrado y narrowing básico, funciona sorprendentemente bien.
Después de dos años con TypeScript 5.x en producción: no actualices en bloque esperando una transformación mágica. Actualiza de forma incremental y adopta activamente isolatedDeclarations si tienes un monorepo, using si manejas recursos con cleanup explícito, y los const genéricos si tienes APIs que se benefician de inferencia más precisa. Los decoradores estables merecen la migración, pero planifícala — no la hagas el viernes por la tarde.
TypeScript 5.x no reinventó el lenguaje. Lo que hizo fue cerrar brechas que llevaban años abiertas, y en producción eso vale más que cualquier feature nueva que suene impresionante en un changelog pero que rara vez tocas en el trabajo real.
Top comments (0)