Decorators
Annotate and extend classes, methods, and fields with reusable decorators
Overview
Decorators are functions that attach reusable behavior or metadata to classes and their members. TypeScript 5 implements the ECMAScript Stage 3 decorator proposal, where a decorator receives the target and a context object and may return a replacement. They are widely used in frameworks for dependency injection, routing, and validation.
Syntax / Usage
A class-method decorator receives the original method and a ClassMethodDecoratorContext, returning a new function that wraps it.
function logged<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This>
) {
const name = String(context.name)
return function (this: This, ...args: Args): Return {
console.log(`calling ${name}`)
return target.apply(this, args)
}
}
class Service {
@logged
fetch(id: number) {
return { id }
}
}
Examples
A class decorator can seal the constructor and its prototype.
function sealed(target: Function, _context: ClassDecoratorContext) {
Object.seal(target)
Object.seal(target.prototype)
}
@sealed
class Registry {}
Use context.addInitializer from a field decorator to bind a method.
function bound<This, V extends (...args: any[]) => any>(
_target: undefined,
context: ClassFieldDecoratorContext<This, V>
) {
context.addInitializer(function (this: This) {
const self = this as Record<string, unknown>
self[context.name as string] = (self[context.name as string] as V).bind(this)
})
return undefined
}
An auto-accessor decorator can validate assignments through set.
function positive(_target: ClassAccessorDecoratorTarget<unknown, number>, _ctx: ClassAccessorDecoratorContext) {
return {
set(this: unknown, value: number) {
if (value < 0) throw new RangeError("must be positive")
},
}
}
Common Mistakes
- Mixing the legacy
experimentalDecoratorsAPI with the Stage 3 signatures - Assuming decorators run per call—they run once at class definition time
- Forgetting to return the wrapped method, so the original is not replaced
- Reading
context.nameas a string withoutString(...), since it may be a symbol - Relying on parameter decorators, which are not part of the Stage 3 standard
See Also
typescript-declaration-files typescript-mapped-types interfaces