TypeScript 5.2: using Declarations and Resource Mgmt
TypeScript 5.2 dropped yesterday. The headline feature is using declarations — automatic resource management via the TC39 Explicit Resource Management proposal. If you’ve ever written a try/finally block just to close a database connection, this one’s for you.
I’ve been tracking this proposal since it hit Stage 3 at TC39 earlier this year. The TypeScript team’s implementation is clean — really clean. Let me walk through what it does, why it matters, and yeah, where it falls short.
The Problem It Solves #
Every Node.js developer has written some version of this:
const connection = await db.connect();
try {
const result = await connection.query('SELECT * FROM users');
// do stuff with result
} finally {
await connection.close();
}It works. It’s also fragile. Forget that finally block and your connection pool leaks. Nest two resources and the indentation gets nasty fast. Add error handling inside the finally and suddenly you’re writing error-handling code for your error-handling code. Not fun.
C# solved this with using statements ages ago. Python has with and context managers. Java has try-with-resources. JavaScript has had… well, nothing. Until now.
How using Works #
The new using keyword tells TypeScript (and eventually JavaScript) to automatically call a cleanup method when the variable goes out of scope:
function readConfig() {
using file = openFile('config.json');
// file is automatically closed when this function returns
return JSON.parse(file.readAll());
}The resource needs to implement Symbol.dispose — a new well-known symbol that acts as the cleanup hook:
class DatabaseConnection {
// ... connection logic
[Symbol.dispose]() {
this.close();
console.log('Connection closed');
}
}When execution leaves the scope where using was declared, Symbol.dispose gets called automatically. Whether the function returns normally or throws an error. No try/finally required. Simple as that.
Async Disposal: await using #
Here’s where it gets interesting for Node developers. Most of our cleanup is async — closing database connections, flushing write streams, disconnecting from message brokers. The proposal handles this with await using and Symbol.asyncDispose:
async function processMessages() {
await using consumer = await kafka.createConsumer();
await using producer = await kafka.createProducer();
// Both consumer and producer are automatically
// disconnected when this function exits.
// Producer disposes first (reverse order).
for await (const message of consumer) {
await producer.send(transform(message));
}
}Disposal happens in reverse declaration order, which matches what you’d expect. The last resource acquired is the first one released. Same as a stack.
One subtlety worth noting: await using works in async functions and module top-level. You can’t use it in a synchronous function even if the disposal itself is async. TypeScript enforces this at compile time — which prevents a whole category of bugs where you’d accidentally drop a disposal promise on the floor.
DisposableStack: Managing Groups #
Sometimes you’re managing a dynamic number of resources. You don’t know at compile time how many connections or handles you’ll need. DisposableStack handles this:
async function migrateTables(tableNames: string[]) {
await using stack = new AsyncDisposableStack();
const connections = tableNames.map(name => {
const conn = await db.connect(name);
stack.use(conn); // Register for automatic disposal
return conn;
});
// All connections close when the function exits,
// even if one of the migrations throws.
await Promise.all(
connections.map(conn => conn.migrate())
);
}DisposableStack itself is disposable. When it disposes, it disposes everything registered with it — in reverse order, of course. You can also register arbitrary cleanup callbacks with stack.defer(() => cleanup()), which is handy for resources that don’t implement Symbol.dispose out of the box.
SuppressedError: When Cleanup Fails #
What happens when disposal throws an error while another error is already in flight? Say your database query fails AND the connection close also fails. Which error do you get?
TypeScript 5.2 introduces SuppressedError for exactly this case. The original error stays as the primary error; the disposal error is attached as SuppressedError.suppressed. You don’t lose either one.
try {
using resource = getFlakeyResource();
throw new Error('operation failed');
// resource[Symbol.dispose]() also throws
} catch (e) {
if (e instanceof SuppressedError) {
console.log('Primary:', e.error); // "operation failed"
console.log('Disposal:', e.suppressed); // disposal error
}
}This is a design choice that Python and C# got wrong initially. Python’s __exit__ can swallow the original exception if it throws — not ideal. Learning from their mistakes.
What This Changes in Practice #
The obvious use case is database connections and file handles. But I think the real impact is broader than that.
Event listeners. How many times have you forgotten to remove an event listener? (Be honest.) Wrap it in a disposable and it cleans itself up.
Temporary state changes. Need to set a flag, do something, then unset it? That’s a disposable. DOM class additions during animations. Environment variable overrides in tests. Temporary permission escalations.
Observability. Wrap a tracing span in a disposable; it automatically closes when the scope ends. No more orphaned spans cluttering your traces because someone forgot span.end().
The pattern works anywhere you have setup/teardown symmetry — which, when you think about it, is most of programming.
The Catch #
TC39 has this at Stage 3, not Stage 4. That means it’s not in the JavaScript standard yet. TypeScript 5.2 is ahead of the runtime — V8 and SpiderMonkey haven’t shipped native support. You’ll need TypeScript’s downlevel emit, which transforms using into try/finally blocks. It works fine, but it’s not zero-cost.
Library adoption will take time. The Symbol.dispose protocol needs to be implemented by database drivers, HTTP clients, stream libraries. Knex, Prisma, pg — none of them support it today. You’ll be writing wrapper classes for a while.
But the direction is right. I’ve wanted something like this in JavaScript for years, and the TypeScript team’s implementation nails the ergonomics. Start playing with it now; by the time your libraries catch up, you’ll already know the patterns.