Modern TC39 decorators for reducing repetitive code in TypeScript.
Features • Installation • Documentation • Usage • Available Decorators
- Built for modern TC39 decorators in TypeScript 5+
- Covers sync and async method workflows
- Includes caching, retry, timeout, debounce, throttling, delegation, rate limiting, lazy evaluation, and resource disposal
$ npm install decorator-toolkit
# or using Bun
$ bun add decorator-toolkitThis package targets the standard TC39 decorator model. It is intended for
TypeScript 5+ projects using standard decorators rather than legacy
experimentalDecorators semantics.
Legacy TypeScript decorators are also available for projects that still use the
older transform. Import them from decorator-toolkit/legacy or
decorator-toolkit/<name>/legacy.
At minimum, use a modern TypeScript configuration that emits native class features and supports standard decorators:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16"
}
}Note
Method decorators in this package apply to methods only, bindAll applies to
classes, and accessor decorators apply to accessor members only. Private
members are not supported.
Tip
Decorators that use default behavior can be written as @decorator or
@decorator(). This applies to bind, bindAll, cancelPrevious,
cache, cacheAsync, delegate, dispose, execTime, lazy, readonly, and
throttleAsync.
import {
after,
before,
retry,
timeout,
} from "decorator-toolkit";
class PaymentService {
private readonly events: string[] = [];
beforeSave(): void {
this.events.push("before");
}
afterSave(params: { args: [string]; response: string; }): void {
this.events.push(`after:${params.args[0]}:${params.response}`);
}
@before<PaymentService>({ func: "beforeSave" })
@after<PaymentService, string, [string]>({ func: "afterSave", wait: true })
@retry(3)
@timeout(1_000)
async save(id: string): Promise<string> {
return `saved:${id}`;
}
}import {
cache,
rateLimit,
} from "decorator-toolkit";
class DirectoryService {
@cache({ ttlMs: 5_000 })
lookupUser(id: string): { id: string; name: string; } {
return { id, name: `user:${id}` };
}
@rateLimit<DirectoryService, [string]>({
allowedCalls: 10,
timeSpanMs: 60_000,
keyResolver: (userId) => userId,
})
openProfile(userId: string): string {
return `/users/${userId}`;
}
}readonly and refreshable are accessor decorators, so they must decorate
accessor members. lazy decorates get accessors directly.
import {
lazy,
readonly,
refreshable,
} from "decorator-toolkit";
class SessionStore {
@readonly
accessor id = crypto.randomUUID();
@lazy
get config(): object {
return buildExpensiveConfig(); // computed once per instance
}
@refreshable<SessionStore, number>({
dataProvider: "loadCounter",
intervalMs: 5_000,
})
accessor counter: number | null = 0;
async loadCounter(): Promise<number> {
return Date.now();
}
}
const store = new SessionStore();
store.counter = null;Assigning null to a refreshable accessor stops future refresh cycles for
that accessor.
You can import from the root package:
import {
delegate,
timeout,
} from "decorator-toolkit";Or import specific modules via subpaths:
import { cache } from "decorator-toolkit/cache";
import {
timeout,
TimeoutError,
} from "decorator-toolkit/timeout";Legacy TypeScript decorators are available from the existing suffix subpaths,
and decorator-toolkit/legacy re-exports the full legacy surface:
import { cache as legacyCache } from "decorator-toolkit/cache/legacy";
import {
cache,
timeout,
} from "decorator-toolkit/legacy";Use the suffix path when you want one decorator only. Use the legacy barrel when you want several legacy decorators from a single import.
Start with docs/README.md for grouped references and usage patterns. The decorator list below links to dedicated pages with current TC39 examples adapted from the legacy site.
| Decorator | Purpose |
|---|---|
| after | Runs a hook after a method call, optionally waiting for async resolution |
| before | Runs a hook before a method call, optionally waiting for async hooks |
| bind | Binds a method to its instance or class during initialization |
| bindAll | Binds all public instance methods declared on a class |
| cancelPrevious | Rejects the previous pending async invocation with CanceledPromise |
| debounce | Coalesces rapid method calls into a later single execution |
| delegate | Shares one in-flight async invocation across callers with the same key |
| delay | Schedules method execution after a fixed delay |
| dispose | Wires a method to Symbol.dispose or Symbol.asyncDispose |
| execTime | Reports method execution duration |
| cache | Caches synchronous method results |
| cacheAsync | Caches async results and deduplicates pending async calls |
| lazy | Computes a getter once per instance and caches the result |
| multiDispatch | Starts multiple async attempts and resolves on the first success |
| onError | Forwards thrown or rejected errors to a handler |
| rateLimit | Limits how many calls may happen within a configured time window |
| readonly | Makes an accessor write-protected |
| refreshable | Refreshes an accessor from an async data provider on an interval |
| retry | Retries async methods using a fixed or custom delay strategy |
| throttle | Limits how often a method can run |
| throttleAsync | Queues async calls and executes them with bounded concurrency |
| timeout | Rejects slow async methods with TimeoutError |