Skip to main content

TypeScript Decorators

A decorator is a special function you attach to a class, a method, a property or an accessor with the @name syntax. It runs when the class is defined and can add metadata to, change, or replace the thing it decorates.

Decorators are used heavily in frameworks like Angular and NestJS for dependency injection, routing and more.


Enabling Decorators

Modern TypeScript (5.0 and later) supports the standard (TC39) decorator syntax out of the box — no compiler option is needed, and that is the form used by every example on this page.

Older code uses the legacy decorator design. To compile that form you must switch it on in tsconfig.json:

  • "compilerOptions": {
  • "target": "ES2020",
  • "module": "commonjs",
  • "experimentalDecorators": true,
  • "emitDecoratorMetadata": true,
  • "strictPropertyInitialization": false
  • },
  • "include": ["src/**/*.ts"]

Note: emitDecoratorMetadata makes the compiler emit type information for legacy decorators. Libraries like TypeORM and class-validator rely on it.


Types of Decorators

TypeScript lets you decorate several kinds of declarations. With standard decorators every decorator receives two arguments: the value being decorated and a context object that tells you its kind and name.

  • Class decorator — applies to class declarations: (constructor: Function, context: ClassDecoratorContext) => void | Function
  • Method decorator — applies to class methods: (method: Function, context: ClassMethodDecoratorContext) => void | Function
  • Accessor decorator — applies to accessor fields (the standard way to intercept reads and writes of a property): (target: { get, set }, context: ClassAccessorDecoratorContext) => { get?, set?, init? }
  • Field decorator — applies to plain class properties: (value: undefined, context: ClassFieldDecoratorContext) => void | ((initialValue) => any)

The legacy design instead passes (target, propertyKey, descriptor) to method decorators, (target, propertyKey) to property decorators and (target, propertyKey, parameterIndex) to parameter decorators. Parameter decorators exist only in the legacy design.


Class Decorators

A class decorator receives the class constructor. You can use it to observe the class, change it, or even return a new constructor that replaces it.

It is called once, when the class is declared — not each time you create an instance.

Basic Class Decorator

This simple decorator logs a message the moment the class is defined.

Prompt
Show a basic class decorator that runs when a class is declared. Write a logClass decorator that takes the class constructor and a ClassDecoratorContext and logs Class <name> was defined at <ISO timestamp>. Apply it with @logClass to a UserService class that has a getUsers() method returning ["Alice", "Bob", "Charlie"]. Nothing else is called — the log line appears as soon as the file loads.
Output: TypeScript file and captured output

Class Decorator with Constructor Modification

This example modifies a class: it adds a version property and returns a wrapper constructor that logs every time an instance is created.

Prompt
Show a class decorator factory that modifies a class and replaces its constructor. Write versioned(version: string) returning a class decorator that sets prototype.version, then returns a wrapper constructor that logs Creating instance of <name> v<version> and calls the original with new, copying the prototype so instanceof keeps working. Apply @versioned("1.0.0") to an ApiClient class with a fetchData() method that logs Fetching data...; create an instance, print its version (cast to any) and call fetchData().
Output: TypeScript file and captured output

Sealed Class Decorator

This decorator stops new properties from being added to the class and marks the existing ones as non-configurable.

Prompt
Show a class decorator that seals a class so no new members can be added. Write a sealed decorator that logs Sealing <name>... and calls Object.seal on both the constructor and its prototype. Apply it with @sealed to a Greeter class holding a greeting: string set in the constructor and a greet() method returning Hello, <greeting>. Leave a commented-out line showing that adding Greeter.prototype.newMethod would fail.
Output: TypeScript file and captured output

Method Decorators

A method decorator is placed on a method and can observe, change or replace it.

It receives two parameters:

  1. originalMethod: the method function itself
  2. context: a ClassMethodDecoratorContext with the method's name, whether it is static or private, and an addInitializer hook

If the decorator returns a function, that function replaces the method. Wrapping the original this way is how you add logging, timing or checks around a call.

Key points about method decorators:

  • They run when the method is defined, not when it is called
  • They can change the method's behavior by wrapping it with extra logic
  • They are used for cross-cutting concerns like logging, validation and authorization
  • They must return a new function or undefined (when nothing is replaced)

Method Timing Decorator

This decorator measures and logs how long a method takes to execute.

Prompt
Show a method decorator that measures how long a method takes to run. Write measureTime(originalMethod, context: ClassMethodDecoratorContext) returning a wrapper that records performance.now() before and after calling the original and logs <method> executed in <ms>ms with two decimals. Apply it to processData(data: number[]): number[] on a DataProcessor class that loops 100,000,000 times to simulate work and returns each value doubled; call it with [1, 2, 3, 4, 5].
Output: TypeScript file and captured output

Method Authorization Decorator

This example implements role-based access control: the decorator checks the current user's roles before letting the method run.

Prompt
Show role-based access control implemented with a method decorator factory. Define type UserRole = "admin" | "editor" | "viewer" and a currentUser object (id 1, name John Doe, roles ["viewer"]). Write AllowedRoles(...roles) returning a method decorator whose wrapper throws User <name> is not authorized to call <method> when the current user has none of the roles. In a DocumentService, allow deleteDocument for admin/editor and viewDocument for all three; inside a try/catch call view then delete as a viewer (the second throws and its message is printed with console.error), then switch roles to ["admin"] and delete again successfully.
Output: TypeScript file and captured output

Deprecation Warning Decorator

This decorator prints a deprecation warning whenever a method that will be removed in a future version is called.

Prompt
Show a method decorator factory that prints a deprecation warning before running the method. Write deprecated(message: string) returning a method decorator whose wrapper calls console.warn with Warning: <method> is deprecated. <message> and then the original method. In a PaymentService mark processPayment(amount, currency) with @deprecated("Use processPaymentV2 instead"), keep processPaymentV2 undecorated, and call both with 100, "USD".
Output: TypeScript file and captured output

Property Decorators

Property decorators let you observe or change how a property is read and written.

With standard decorators the property is declared with the accessor keyword. The decorator then receives two parameters:

  1. target: an object with the generated get and set functions
  2. context: a ClassAccessorDecoratorContext with the property's name

Returning an object with your own get and set replaces the default accessors.

Key points about property decorators:

  • They run when the class is defined, not when the property is accessed
  • A decorator on a plain (non-accessor) field can only adjust the initial value; to intercept reads and writes use accessor
  • In the legacy design they receive (target, propertyKey) and must use Object.defineProperty to change behavior
  • They are often used for formatting, logging, validation or metadata
  • They can be combined with other decorators for more complex behavior

Formatted Property Decorator

This decorator automatically formats a property every time it is set.

Prompt
Show a property (auto-accessor) decorator factory that formats a value every time it is set. Write format(formatString: string) returning an accessor decorator that keeps a private value and returns a get/set pair where the setter stores formatString with {} replaced by the new value. Apply @format("Hello, {}!") to accessor greeting: string in a Greeter class, set greeting to "World" and print it (expected Hello, World!).
Output: TypeScript file and captured output

Logging Property Decorator

This decorator logs every read and every change of a property.

Prompt
Show an accessor decorator that logs every read and write of a property. Write logProperty that returns a getter logging Getting <name>: <value> and a setter logging Setting <name> from <old> to <new>. Apply it to accessor name: string and accessor price: number of a Product class whose constructor assigns both. Create new Product("Laptop", 999.99), set price to 899.99, then print product.name.
Output: TypeScript file and captured output

Required Property Decorator

This decorator makes sure a property has been given a value before it is read.

Prompt
Show an accessor decorator that enforces a property must have a value before it is read. Write required whose getter throws Property <name> is required when the stored value is undefined and whose setter simply stores the value. Apply it to accessor username: string and accessor email: string of a User class (with an optional age?: number) whose constructor sets both. Create new User("johndoe", "[email protected]"), which works, and leave commented-out lines showing that passing undefined would throw. The program prints nothing.
Output: TypeScript file and captured output

Parameter Decorators

In the legacy decorator design you can also decorate a single parameter of a constructor or method. Such a decorator receives three parameters:

  1. target: the prototype of the class (for instance methods) or the constructor (for static methods)
  2. propertyKey: the name of the method (or undefined for constructor parameters)
  3. parameterIndex: the position of the parameter in the parameter list

Standard decorators do not have parameter decorators. The example below keeps the idea — describe a rule per parameter, then check the arguments when the method is called — but passes the rules to a method decorator in parameter order, so it compiles with default settings.

Key points about parameter decorators:

  • They run when the method is defined, not when it is called
  • They are usually paired with a method decorator that does the real work
  • With the reflect-metadata library they can store and read metadata
  • They are common in dependency injection frameworks
  • The parameter index lets you find the matching argument at runtime

Parameter Validation Decorator

This example validates method parameters: each rule says which type is expected at which position, and the method decorator checks the arguments on every call.

Prompt
Show parameter validation driven by decorators, written with standard (TC39) decorators, which have no parameter decorators. Write validateParam(type) that returns a rule object for "string" | "number" | "boolean", and a validate(...rules) method decorator that maps the rules to { index, type } entries and, on every call, checks each argument (non-empty string, non-NaN number, boolean) and throws Parameter at index <i> failed <type> validation on failure. Apply @validate(validateParam("string"), validateParam("number"), validateParam("boolean")) to createUser(name, age, isActive) on a UserService that logs Creating user: <name>, <age>, <isActive>; call it with "John", 30, true and leave commented-out failing calls.
Output: TypeScript file and captured output

Decorator Factories

A decorator factory is a function that returns a decorator.

Because you call the factory with arguments, you can configure the decorator it produces. This makes decorators more flexible and reusable.

Configurable Logging Decorator

This example builds a logging decorator that can be customized with a log level and a message.

Prompt
Show a decorator factory that takes a configuration object. Write logWithConfig(config: { level: "log" | "warn" | "error", message?: string }) returning a method decorator whose wrapper prints <message>: <method> plus the arguments through console[level], runs the original method, then prints <method> completed. Apply it with level log and message Processing payment to processPayment(amount: number) in a PaymentService that logs Processing payment of $<amount>. Only declare the class — nothing is called, so there is no output.
Output: TypeScript file and captured output

Order of Evaluation

When several decorators are stacked on one declaration, the factories are evaluated top to bottom, and then the decorators they returned are called bottom to top.

Prompt
Show the order in which decorator factories and the decorators they return are evaluated when several are stacked on one method. Write two factories first() and second() that each log <name>(): factory evaluated and return a method decorator that logs <name>(): called. Stack @first() above @second() on an empty method() in ExampleClass. Expected order: both factories top to bottom, then the decorators bottom to top (second(): called before first(): called).
Output: TypeScript file and captured output

Real-World Examples

API Controller with Decorators

This example uses decorators to describe a small API controller, similar to what NestJS or Express-based frameworks do.

Prompt
Show how decorators can register routes for a small API controller, in the style of NestJS or Express. Keep a ROUTES array. Write a Controller(prefix) class decorator that stores the prefix on the prototype and assigns the class to routes collected so far, and a Get(path) method decorator that pushes { method: "get", path, handler }. Decorate a UserController with @Controller("/users") and two methods @Get("/") getAllUsers() and @Get("/:id") getUserById(id). A registerRoutes() function logs Registered GET <prefix><path> for every route.
Output: TypeScript file and captured output

Best Practices

Follow these practices when you write decorators:

  1. Keep decorators focused: each decorator should do one thing.
  2. Document behavior: say clearly what a decorator does and which side effects it has.
  3. Use decorator factories: make decorators configurable so they can be reused.
  4. Consider performance: a decorator that wraps a method adds work to every call.
  5. Type safety: use the context types (ClassMethodDecoratorContext and friends) to keep decorators type-safe.
  6. Error handling: handle errors properly inside decorators.
  7. Testing: write unit tests for your decorators.
  8. Metadata: use reflect-metadata for advanced cases that need runtime type information.

Common Pitfalls

Avoid these common mistakes:

  • Wrong decorator flavor: legacy-style decorators need experimentalDecorators in tsconfig.json; standard decorators must not have it.
  • Incorrect decorator signature: each decorator kind has its own parameters. The wrong ones lead to runtime errors.
  • Evaluation order: stacked decorators are applied bottom to top.
  • Property initialization: property decorators run before instance properties are initialized.
  • Metadata reflection: forgetting to import reflect-metadata when you use decorator metadata.
  • Performance overhead: be careful with heavy decorators in performance-critical code.
  • Browser compatibility: decorators are a stage 3 proposal, so older browsers need transpiled output.

The two snippets below use the legacy syntax. To compile them, add this to tsconfig.json: "compilerOptions": { "experimentalDecorators": true }.

Where are decorators used?

  • Angular: components, services, modules and more
  • NestJS: controllers, providers, routes and more

Legacy readonly property decorator

A legacy-style property decorator that uses Object.defineProperty to make name non-writable.

script.ts
function readonly(target: any, propertyKey: string) {
  Object.defineProperty(target, propertyKey, {
    writable: false
  });
}

class Person {
  @readonly
  name = "John";
}

Legacy parameter decorator

A legacy-style parameter decorator that logs the method name and the index of the decorated parameter.

script.ts
function logParameter(target: any, propertyKey: string, parameterIndex: number) {
  console.log(`Parameter in ${propertyKey} at index ${parameterIndex}`);
}

class Demo {
  greet(@logParameter message: string) {
    return message;
  }
}