TypeScript Decorators
Implement standard TC39 decorators and legacy metadata reflection to intercept class methods, properties, accessors, and constructors.
TL;DR
- Apply standard TC39 decorators using the
@symbol prefix. - Intercept and wrap method invocations using
ClassMethodDecoratorContextcallbacks. - Enable
experimentalDecoratorsonly when supporting legacy frameworks like NestJS.
Standard Class Decorators
Class decorator definitionReceive target constructor and class context.
function Frozen<T extends new (...args: any[]) => any>(
target: T,
ctx: ClassDecoratorContext
) {
Object.freeze(target);
Object.freeze(target.prototype);
}Applying class decoratorDecorate class with prefix at-symbol.
@Frozen
class ConfigRegistry {
appName = "AnalyticsEngine";
}Class replacement decoratorReturn custom subclass to extend behaviors.
function Timestamped<
T extends new (...args: any[]) => any
>(
Base: T,
ctx: ClassDecoratorContext
) {
return class extends Base {
createdAt = new Date();
};
}Method Decorators
Method logger decoratorWrap execution with logging hooks.
function LogMethod(
original: Function,
ctx: ClassMethodDecoratorContext
) {
return function (this: any, ...args: any[]) {
console.log(`Called ${String(ctx.name)}`, args);
return original.apply(this, args);
};
}Applying method decoratorAnnotate instance methods cleanly.
class TaskRunner {
@LogMethod
execute(taskId: string): void {
console.log(`Running task ${taskId}`);
}
}Performance timer decoratorMeasure execution duration of methods.
function Benchmark(
fn: Function,
ctx: ClassMethodDecoratorContext
) {
return function (this: any, ...args: any[]) {
const t0 = performance.now();
const res = fn.apply(this, args);
console.log(`${performance.now() - t0}ms`);
return res;
};
}Field and Accessor Decorators
Field initializer decoratorTransform initial values assigned to fields.
function Upper(
target: undefined,
ctx: ClassFieldDecoratorContext
) {
return function (initialValue: string) {
return initialValue.toUpperCase();
};
}Applying field decoratorDecorate instance field declarations.
class User {
@Upper
role: string = "admin";
}Auto-accessor decoratorIntercept getter and setter on accessors.
function Validated(
target: ClassAccessorDecoratorTarget<any, number>,
ctx: ClassAccessorDecoratorContext
) {
return {
set(this: any, val: number) {
if (val < 0) throw new RangeError("Negative");
target.set.call(this, val);
}
};
}Legacy Experimental Decorators
Experimental compiler flagsEnable legacy flags for Angular and NestJS.
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Legacy method descriptorMutate property descriptor directly.
// For experimentalDecorators: true
// function Log(target: any, key: string, desc: any) {
// const fn = desc.value;
// }Metadata reflection importImport polyfill for reflect metadata.
// import "reflect-metadata";
// Reflect.defineMetadata("key", "val", Target);Parameter decorator stubMark parameters for dependency injection.
// function Inject(token: string) {
// return (target: any, key: string, i: number) => {
// };
// }Tips
- Use standard TypeScript 5.0 decorators whenever possible, as they require zero
tsconfig.jsoncompiler flags and conform directly to ECMAScript specifications. - Place decorators on separate lines directly above targets with
@decoratorsyntax to preserve clean readability across complex class definitions.
Warnings
- Legacy decorators using
experimentalDecoratorsand standard TC39 Stage 3 decorators have incompatible signatures; never mix their runtime helper functions. - Mutating class prototypes directly inside method decorators can trigger unexpected side effects across subclasses sharing the same prototype
prototypechain.
In Practice
A production-grade TypeScript 5 method decorator that caches expensive calculation outputs based on serialized arguments.
- Define a decorator function accepting a method and method context.
- Initialize an in-memory cache map scoped to the method.
- Serialize argument parameters into cache lookup keys.
- Return cached values or delegate to the original implementation.
function Memoize(
fn: Function, ctx: ClassMethodDecoratorContext
) {
const cache = new Map<string, any>();
return function (this: any, ...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const res = fn.apply(this, args);
cache.set(key, res);
return res;
};
}
class MathService {
@Memoize
fib(n: number): number {
if (n <= 1) return n;
return this.fib(n - 1) + this.fib(n - 2);
}
}FAQ
Standard decorators (available in TypeScript 5.0+) conform to the TC39 Stage 3 specification and work without any compiler flags. The older experimentalDecorators flag uses a legacy non-standard API required by older versions of Angular and NestJS. Standard decorators receive an element and a context object instead of target, propertyKey, and descriptor.
Decorator expressions are evaluated top-down (outermost to innermost), but the resulting decorator functions are called bottom-up (innermost to outermost). The outermost decorator wraps around the output of the inner decorators, having the final say on the transformed method or class.
A standard method decorator is a function taking the original method and a ClassMethodDecoratorContext. It can return a replacement function that executes logic before or after delegating to the original method, enabling logging, caching, or access control.
Yes, standard decorators support the accessor keyword on class fields. An accessor decorator receives a getter/setter pair and a ClassAccessorDecoratorContext, returning a customized getter and setter to intercept read and write operations on the state variable.
The reflect-metadata polyfill and emitDecoratorMetadata flag are used exclusively with legacy experimental decorators to inspect runtime types for dependency injection containers. Standard TC39 decorators do not emit TypeScript type metadata.