typescript 41 lines · 9 steps

A method decorator that times async calls

A configurable TypeScript decorator wraps an async method to log how long it takes, but only past a threshold.

Explained by highlit
1type AsyncMethod = (...args: any[]) => Promise<any>;
2 
3function LogExecutionTime(thresholdMs = 0) {
4 return function <T extends AsyncMethod>(
5 target: object,
6 propertyKey: string | symbol,
7 descriptor: TypedPropertyDescriptor<T>,
8 ): TypedPropertyDescriptor<T> {
9 const original = descriptor.value;
10 if (!original) {
11 throw new Error(`@LogExecutionTime can only decorate methods`);
12 }
13 
14 const label = `${target.constructor.name}.${String(propertyKey)}`;
15 
16 descriptor.value = async function (this: unknown, ...args: unknown[]) {
17 const start = performance.now();
18 try {
19 return await original.apply(this, args);
20 } finally {
21 const elapsed = performance.now() - start;
22 if (elapsed >= thresholdMs) {
23 console.info(`[perf] ${label} took ${elapsed.toFixed(2)}ms`);
24 }
25 }
26 } as T;
27 
28 return descriptor;
29 };
30}
31 
32class ReportService {
33 @LogExecutionTime(50)
34 async generate(userId: string): Promise<Buffer> {
35 const rows = await this.repo.fetchRows(userId);
36 return this.renderer.toPdf(rows);
37 }
38 
39 private repo!: { fetchRows(id: string): Promise<unknown[]> };
40 private renderer!: { toPdf(rows: unknown[]): Buffer };
41}
01 / 01
STEP 01

Walkthrough

Space play ←→ step click any line
Three takeaways
  1. 1Decorator factories take arguments and return the real decorator, letting you configure behavior per usage.
  2. 2Wrapping the original method while preserving `this` via `apply` keeps the decorated method behaving identically.
  3. 3A `finally` block guarantees timing is recorded whether the wrapped call resolves or throws.

Related explainers

Share this explainer

Here's the card — post it anywhere.

A method decorator that times async calls — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code