Section 5 · Shipping TypeScript
using Declarations and Standard Decorators
17 min4 min readCode playground
Lesson at a glance
Release resources reliably with using declarations and Symbol.dispose, and write typed standard decorators for cross-cutting behaviour such as logging and retries.
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Cartwheel’s checkout reserves stock before charging a card, then releases the reservation if anything fails. The first version of that code looked like this:
function placeOrder(skus: string[]) {
const reservations = skus.map((sku) => reserve(sku));
chargeCard(); // throws on a declined card
reservations.forEach((r) => r.release());
}A declined card skips the last line, and the stock stays locked until a nightly job notices. The traditional fix is try/finally, nested once per resource. This lesson covers the language’s built-in answer, and then a second modern feature that keeps cross-cutting code like retries out of your business logic: standard decorators.
using: cleanup tied to scope
A using declaration is like const, with one addition: when the block it lives in exits, by return, by falling off the end, or by throw, JavaScript calls the value’s [Symbol.dispose]() method.
function reserve(sku: string): Disposable {
log(`reserve ${sku}`);
return { [Symbol.dispose]: () => log(`release ${sku}`) };
}
function placeOrder() {
using tent = reserve('TENT-2P');
using mug = reserve('MUG-SET');
chargeCard(); // even if this throws…
} // …both are released here: MUG-SET first, then TENT-2PSeveral using declarations are disposed in reverse order, exactly like nested finally blocks, so a resource is always released before the ones it was created after. If disposal itself throws while another error is in flight, both are kept in a SuppressedError rather than one silently replacing the other.
For cleanup that is asynchronous, such as closing a database transaction, use await using with an object that has [Symbol.asyncDispose]() returning a promise. And when the number of resources is dynamic, collect them in a DisposableStack (or AsyncDisposableStack), which is itself disposable:
function placeOrder(skus: string[]) {
using stack = new DisposableStack();
for (const sku of skus) stack.use(reserve(sku));
chargeCard();
} // every reservation released, in reverse orderusing arrived in TypeScript 5.2. It runs natively in Node 24, Chrome and Edge 134+ and Firefox 141+, but Safari does not support it yet (as of late 2026), so it is not Baseline. For code that ships to browsers, let TypeScript or your bundler downlevel the syntax, and make sure the runtime has Symbol.dispose (a polyfill provides it where it is missing). The types live in the esnext.disposable lib (included in "lib": ["esnext"]). The exercise playground uses the ES2022 lib, so its starter declares those two pieces itself, which is a preview of the declaration merging you will learn in the next lesson.
Making your own APIs disposable
using pays off most when your own APIs return disposables. The emitter from Type-Safe Event Emitters and Builders returned an off function; returning an object with [Symbol.dispose] as well lets callers scope a subscription to a block:
function subscribe<K extends keyof CheckoutEvents>(event: K, handler: (p: CheckoutEvents[K]) => void): Disposable {
const off = bus.on(event, handler);
return { [Symbol.dispose]: off };
}
async function waitForShipment(orderId: number) {
using _ = subscribe('order.shipped', (e) => { if (e.orderId === orderId) notify(e); });
await pollUntilShipped(orderId);
} // listener removed here, however the function endsGood candidates are anything with a matching “undo”: locks and reservations, event subscriptions, timers, temporary files, database transactions (with await using). If you find yourself writing try/finally only to call a cleanup method, that resource wants a [Symbol.dispose].
Standard decorators
A decorator is a function applied with @ to a class or class member. TypeScript 5.0 implemented the standard (TC39) decorators, and TypeScript compiles them down to ordinary function calls, so they run in any runtime. A method decorator receives the original method and a context object, and may return a replacement:
function logged<This, Args extends unknown[], Return>(
method: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>,
) {
const name = String(context.name);
return function (this: This, ...args: Args): Return {
console.log(`→ ${name}`, args);
return method.call(this, ...args);
};
}
class PaymentGateway {
@logged
charge(amountCents: number, currency: string) { /* … */ }
}The generics make the decorator work on any method without any: This, Args and Return are inferred from the method it decorates, and the replacement keeps the same signature. Decorators that take options are decorator factories, functions that return the decorator:
function retry(times: number) {
return function <This, Args extends unknown[], Return>(
method: (this: This, ...args: Args) => Return,
_context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>,
) {
return function (this: This, ...args: Args): Return {
for (let attempt = 1; ; attempt++) {
try {
return method.call(this, ...args);
} catch (e) {
if (attempt >= times) throw e;
}
}
};
};
}
class PaymentGateway {
@retry(3)
charge(amountCents: number, currency: string) { /* … */ }
}Stacked decorators apply from the bottom up. With @logged above @retry(3), the retry wrapper is applied to charge first and logged wraps the result, so you get one log line per call rather than one per attempt. Swap them and you log every attempt. Neither is wrong, but the order is behaviour, so choose it on purpose.
context also offers context.name, context.static, context.private and context.addInitializer(fn), which runs code when an instance is created (the usual way to write a @bound decorator that binds a method to its instance).
In the exercise you will release stock reservations with using and add a typed @retry to Cartwheel’s payment gateway. Next lesson: describing code you did not write, with declaration files.
Key takeaways
- A
usingdeclaration calls the value’s[Symbol.dispose]()when the enclosing block exits, whether it returns normally or throws. - Several
usingdeclarations in one block are disposed in reverse order, like nestedtry/finallyblocks. await usingandSymbol.asyncDisposedo the same for cleanup that returns a promise.- Standard decorators (TypeScript 5.0+) are functions of
(value, context)that can return a replacement; they are not the same as the olderexperimentalDecorators. - Type a method decorator with
ClassMethodDecoratorContextand genericThis,ArgsandReturnparameters so it works on any method withoutany.