El problema del ciclo de vida de recursos
En JavaScript y TypeScript conviven muchos tipos de recursos con ciclo de vida: descriptores de fichero, conexiones a bases de datos, suscripciones, temporizadores o mutexes. La práctica habitual consiste en envolver cada uso en un bloque try/finally que garantice la liberación aunque la operación principal falle.
function leerConfiguracion(ruta: string): string {
const fichero = abrirFichero(ruta)
try {
return fichero.leerTodo()
} finally {
fichero.cerrar()
}
}
El patrón funciona, pero se vuelve ruidoso cuando hay varios recursos anidados. Cada nivel añade indentación, es fácil olvidar un cierre y el orden de liberación depende del orden manual de los bloques.
Python resolvió este problema con la sentencia "with" y los context managers. Java incorporó "try-with-resources". TypeScript estrena ahora su propia solución basada en la propuesta de ECMAScript Explicit Resource Management.
Las declaraciones using y await using
TypeScript permite declarar una variable con la palabra clave using para objetos que implementan Symbol.dispose. Al salir del bloque que contiene la declaración, el motor invoca automáticamente el método de liberación. La versión asíncrona, await using, espera el resultado de un Symbol.asyncDispose.
function leerConfiguracion(ruta: string): string {
using fichero = abrirFichero(ruta)
return fichero.leerTodo()
// Al salir de la función se invoca fichero[Symbol.dispose]()
}
async function consultarUsuarios(): Promise<Usuario[]> {
await using conexion = await abrirConexion()
return conexion.query("select * from usuarios")
// Al salir se invoca conexion[Symbol.asyncDispose]()
}
El compilador añade la semántica de liberación al generar el código emitido, equivalente a un try/finally implícito pero sin la ceremonia. El valor declarado con using es de solo lectura: no se puede reasignar, lo que impide invalidarlo a mitad de vida.
flowchart LR
A[Entrada al bloque] --> B[using recurso = crear]
B --> C[Uso del recurso]
C --> D{Salida del bloque}
D -->|Normal o excepcion| E[Symbol.dispose o asyncDispose]
E --> F[Fin del bloque]
Implementar Disposable y AsyncDisposable
Para que una clase propia sea compatible con using, debe definir el método con la clave de símbolo correspondiente. TypeScript expone los tipos Disposable y AsyncDisposable que describen ese contrato.
class LectorFichero implements Disposable {
constructor(private ruta: string) {
console.log(`Abriendo ${ruta}`)
}
leer(): string {
return "contenido simulado"
}
[Symbol.dispose](): void {
console.log(`Cerrando ${this.ruta}`)
}
}
function ejemplo() {
using lector = new LectorFichero("datos.txt")
console.log(lector.leer())
// Al salir, se imprime "Cerrando datos.txt"
}
Para recursos asíncronos se implementa AsyncDisposable, que devuelve una promesa:
class ConexionBD implements AsyncDisposable {
async query(sql: string): Promise<unknown[]> {
return []
}
async [Symbol.asyncDispose](): Promise<void> {
console.log("Cerrando conexion")
}
}
async function consulta() {
await using conexion = new ConexionBD()
await conexion.query("select 1")
}
La clase decide qué hacer al liberar: cerrar sockets, hacer rollback de una transacción, cancelar un intervalo o devolver un objeto a un pool. El contrato se expresa en el tipo y el compilador aplica la comprobación estructural habitual.
Combinar varios recursos con DisposableStack
Cuando un bloque crea varios recursos o recibe alguno desde fuera, conviene agruparlos. La biblioteca estándar define DisposableStack y AsyncDisposableStack, que recogen recursos y los liberan en orden inverso al registro, como hace una pila.
function importar(rutaEntrada: string, rutaSalida: string) {
using pila = new DisposableStack()
const entrada = pila.use(abrirFichero(rutaEntrada))
const salida = pila.use(abrirFichero(rutaSalida))
for (const linea of entrada.lineas()) {
salida.escribir(linea.toUpperCase())
}
// pila libera primero salida y despues entrada
}
El método use registra un objeto Disposable. También existen defer para registrar funciones sueltas y adopt para adoptar valores con una función de liberación ad hoc. La variante asíncrona AsyncDisposableStack acepta recursos AsyncDisposable y se declara con await using.
Configuración en tsconfig.json
Para compilar código que utiliza using, el target debe incluir soporte a Symbol.dispose o bien la bibliotecas de polyfills oficiales. La configuración mínima recomendada:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "ESNext.Disposable", "DOM"],
"module": "NodeNext",
"strict": true
}
}
Runtimes como Node.js 22 LTS, Bun y Deno ya implementan los símbolos de forma nativa. En navegadores y runtimes antiguos se usa el polyfill de @types/node o la biblioteca disposablestack.
Patrones habituales con using
La semántica determinista de using encaja muy bien con transacciones, bloqueos y suscripciones:
class Transaccion implements AsyncDisposable {
private confirmada = false
async commit(): Promise<void> {
this.confirmada = true
}
async [Symbol.asyncDispose](): Promise<void> {
if (!this.confirmada) {
await this.rollback()
}
}
private async rollback(): Promise<void> {
console.log("Rollback automatico")
}
}
async function transferir(origen: string, destino: string, importe: number) {
await using tx = await iniciarTransaccion()
await cargar(origen, importe)
await abonar(destino, importe)
await tx.commit()
// Si cualquier paso lanza, Symbol.asyncDispose ejecuta rollback
}
El patrón unifica la ruta feliz y la ruta de error en un único flujo lineal, elimina bloques anidados y deja la intención del código visible en la primera línea de cada recurso.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de TypeScript
Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Comprender el problema del ciclo de vida de recursos y por qué try/finally resulta verboso. Usar la declaración using para liberar recursos sincronos al salir de ámbito. Aplicar await using para recursos asíncronos. Implementar Disposable y AsyncDisposable en clases propias. Combinar varios recursos con DisposableStack para liberar en orden inverso.