decorator-toolkit
v0.4.0
Published
Modern TC39 decorators for reducing repetitive code.
Downloads
68
Readme
Features
- 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
Installation
npm
$ npm install decorator-toolkit
# or using Bun
$ bun add decorator-toolkitUsage
This 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.
Compiler Setup
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,
bindAllapplies to classes, and accessor decorators apply toaccessormembers only. Private members are not supported.
[!TIP] Decorators that use default behavior can be written as
@decoratoror@decorator(). This applies tobind,bindAll,cancelPrevious,cache,cacheAsync,delegate,dispose,execTime,lazy,readonly, andthrottleAsync.
Basic Example
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}`;
}
}Caching And Rate Limiting
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}`;
}
}Accessor Decorators
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.
Root And Subpath Imports
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.
Documentation
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.
Available Decorators
| 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 |
