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