Typescript

How to implement a typescript decorator

20 September 2026 · 13 min read

How to implement a typescript decorator

TypeScript decorators offer a powerful and elegant way to add metadata or modify the behavior of classes, methods, properties, or parameters. If you’re diving into advanced TypeScript development, understanding how to implement a TypeScript decorator is crucial for writing cleaner, more maintainable, and more expressive code. Decorators provide a declarative syntax, allowing you to inject functionality without directly altering the underlying class or its members. This separation of concerns enhances code reusability and testability. Many popular frameworks, like Angular and NestJS, heavily rely on decorators, showcasing their importance in modern application development. This article breaks down the process of creating and applying decorators, giving you the knowledge to leverage this powerful feature in your own projects. We’ll explore different types of decorators, provide practical examples, and address common use cases.

Understanding TypeScript Decorators

TypeScript decorators are essentially functions that can be applied to declarations such as classes, methods, properties, or parameters. They use the @expression syntax, where expression evaluates to a function that will be called at runtime with information about the decorated declaration. According to the official TypeScript documentation [^1], decorators are an experimental feature but have been widely adopted due to their utility and expressive power. The experimental support must be enabled in your tsconfig.json file by setting the experimentalDecorators compiler option to true.

Decorators can be used for various purposes, including logging, validation, dependency injection, and more. By using decorators, you can avoid writing repetitive boilerplate code and keep your classes focused on their core responsibilities. For example, you might create a decorator that automatically logs method calls or enforces type checking at runtime. This helps in creating more robust and maintainable applications. Consider them as a way to perform metaprogramming, altering the behavior of code based on annotations.

There are different types of decorators, including class decorators, method decorators, accessor decorators, property decorators, and parameter decorators. Each type serves a specific purpose and receives different arguments when invoked. Understanding these differences is key to effectively using decorators in your projects. Class decorators are applied to the constructor of a class, while method decorators are applied to methods within a class. Property decorators work on class properties, and parameter decorators operate on function parameters. Accessor decorators apply to the get and set accessors of a class property.

Implementing Class Decorators

Class decorators are applied to the constructor of a class and can be used to observe, modify, or replace a class definition. They are declared using the @ symbol followed by the decorator function name. The decorator function receives the class constructor as its argument, allowing you to manipulate the class’s prototype or even replace the entire constructor. This enables powerful metaprogramming capabilities. For instance, you can add new methods or properties to the class, modify existing ones, or even create a completely new class based on the original one.

Here’s an example of a class decorator that adds a createdAt property to a class:

typescript function CreatedAt(constructor: Function) { constructor.prototype.createdAt = new Date(); } @CreatedAt class MyClass { constructor() { // … } } const instance = new MyClass(); console.log(instance.createdAt); // Output: Date object representing the creation time In this example, the CreatedAt decorator is applied to MyClass. The decorator function adds a createdAt property to the prototype of MyClass, effectively adding it to all instances of the class. This demonstrates how class decorators can inject behavior into a class without directly modifying its code. Class decorators are particularly useful for tasks like dependency injection, logging, or applying configuration settings to classes.

To further illustrate, let’s say we want to create a decorator that automatically registers a class with a dependency injection container. This decorator could add metadata to the class that the container can use to resolve dependencies. This is a common pattern in frameworks like Angular and NestJS, where decorators are used extensively for dependency injection and module configuration. The official NestJS documentation provides extensive examples [^2].

Working with Method Decorators

Method decorators are applied to methods within a class and can be used to intercept method calls, modify arguments, or alter the return value. They are declared using the @ symbol followed by the decorator function name, just like class decorators. However, method decorators receive different arguments: the target object (either the class prototype or the class constructor for static methods), the method name as a string, and the property descriptor for the method. This provides fine-grained control over method behavior.

One common use case for method decorators is logging method calls. You can create a decorator that automatically logs the method name, arguments, and return value whenever the method is called. This can be invaluable for debugging and auditing purposes. Consider this example:

typescript function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function(…args: any[]) { console.log(Calling method: ${propertyKey} with arguments: ${JSON.stringify(args)}); const result = originalMethod.apply(this, args); console.log(Method ${propertyKey} returned: ${result}); return result; }; return descriptor; } class MyClass { @LogMethod add(x: number, y: number) { return x + y; } } const instance = new MyClass(); instance.add(2, 3); // Logs the method call and return value to the console In this example, the LogMethod decorator wraps the add method with a logging function. Every time add is called, the decorator logs the method name, arguments, and return value to the console. This demonstrates how method decorators can add cross-cutting concerns to methods without modifying their core logic. According to research from the IEEE [^3], such decorators can reduce debugging time by up to 20%.

Another real-world example is implementing caching. You could create a decorator that caches the return value of a method based on its arguments, improving performance by avoiding redundant computations. This is particularly useful for methods that perform expensive operations or fetch data from external sources. You could also use method decorators to implement access control, ensuring that only authorized users can call certain methods.

Property and Parameter Decorators

Property decorators are applied to properties within a class and can be used to observe or modify the property’s behavior. They receive the target object (either the class prototype or the class constructor for static properties) and the property name as a string. Parameter decorators, on the other hand, are applied to parameters of a method or constructor and receive the target object, the method name, and the index of the parameter in the parameter list.

Property decorators can be used for tasks like validating property values or automatically initializing properties with default values. They can also be used to create computed properties or to implement data binding. For example, consider a property decorator that ensures that a property value is always a valid email address:

typescript function ValidateEmail(target: any, propertyKey: string) { let value: string; const getter = function() { return value; }; const setter = function(newVal: string) { if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newVal)) { throw new Error(Invalid email address: ${newVal}); } value = newVal; }; Object.defineProperty(target, propertyKey, { get: getter, set: setter, enumerable: true, configurable: true, }); } class User { @ValidateEmail email: string; } const user = new User(); user.email = ‘invalid-email’; // Throws an error user.email = ‘valid@email.com’; // Works fine This ValidateEmail decorator intercepts property assignments and throws an error if the new value is not a valid email address. This ensures that the email property always contains a valid email address, improving data integrity. This is particularly useful when dealing with user input or data from external sources.

Parameter decorators are less commonly used than class, method, or property decorators, but they can be useful for tasks like injecting dependencies into constructor parameters or validating parameter values. For instance, a parameter decorator can be used to automatically inject a configuration value into a constructor parameter based on its type. Here are some key points about decorators:

  • Decorators provide a declarative syntax for adding metadata or modifying behavior.
  • Different types of decorators exist for classes, methods, properties, and parameters.
  • Enable the experimentalDecorators compiler option in tsconfig.json to use decorators.

And some benefits of using decorators:

  • Improved code readability and maintainability.
  • Reduced boilerplate code.
  • Enhanced code reusability and testability.

Best Practices and Advanced Techniques

When working with TypeScript decorators, it’s important to follow best practices to ensure that your code is clean, maintainable, and performant. One key best practice is to keep your decorator functions small and focused. Decorators should ideally perform a single, well-defined task. Avoid complex logic within decorators, as this can make your code harder to understand and debug. Instead, encapsulate complex logic in separate functions or classes and call them from within the decorator.

Another important best practice is to use descriptive names for your decorators. The name of a decorator should clearly indicate its purpose. This makes it easier for other developers to understand what the decorator does and how it should be used. For example, a decorator that logs method calls should be named something like LogMethod or Log. Avoid generic names like Decorator or Util, as these don’t provide any information about the decorator’s purpose.

Advanced techniques include using decorator factories, which are functions that return decorators. This allows you to pass arguments to your decorators, making them more flexible and reusable. For example, you could create a logging decorator that accepts a log level as an argument, allowing you to control the verbosity of the logging. Here’s an example of how to use a decorator factory:

typescript function Log(level: string) { return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function(…args: any[]) { console.log([${level}] Calling method: ${propertyKey} with arguments: ${JSON.stringify(args)}); const result = originalMethod.apply(this, args); console.log([${level}] Method ${propertyKey} returned: ${result}); return result; }; return descriptor; }; } class MyClass { @Log(‘DEBUG’) add(x: number, y: number) { return x + y; } } In this example, the Log function is a decorator factory that accepts a level argument. The decorator returned by the factory logs method calls with the specified log level. This makes the Log decorator more flexible and reusable, as you can use it with different log levels depending on your needs. Proper understanding of the decorator pattern is key to mastering this technique.

Featured Snippet: A key aspect of implementing TypeScript decorators involves enabling the ’experimentalDecorators’ compiler option within your tsconfig.json file. This setting tells the TypeScript compiler to allow the use of decorator syntax. Without this enabled, the compiler will throw errors when it encounters decorator syntax, preventing your code from compiling successfully. Ensure this is set to ’true’ to leverage the full power of TypeScript decorators.

Here’s how to enable the compiler option:

  1. Open your tsconfig.json file.
  2. Locate the compilerOptions section.
  3. Add or modify the experimentalDecorators property to true.
  4. Save the file and recompile your TypeScript code.

FAQ

What is a TypeScript decorator?
A TypeScript decorator is a special kind of declaration that can be attached to a class, method, accessor, property, or parameter. Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime.
Why use TypeScript decorators?
Decorators provide a clean and declarative way to add metadata or modify the behavior of code without directly altering the underlying code. This improves code reusability, maintainability, and readability.
How do I enable decorators in TypeScript?
You need to enable the experimentalDecorators compiler option in your tsconfig.json file by setting it to true.
What are the different types of decorators?
The main types of **Question & Answer :** [TypeScript 1.5](http://blogs.msdn.com/b/typescript/archive/2015/03/27/announcing-typescript-1-5-alpha.aspx) now has [decorators](https://github.com/wycats/javascript-decorators).

Could someone provide a simple example demonstrating the proper way to implement a decorator and describe what the arguments in the possible valid decorator signatures mean?

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void; declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void; declare type ParameterDecorator = (target: Function, propertyKey: string | symbol, parameterIndex: number) => void; 

Additionally, are there any best practice considerations that should be kept in mind while implementing a decorator?

I ended up playing around with decorators and decided to document what I figured out for anyone who wants to take advantage of this before any documentation comes out. Please feel free to edit this if you see any mistakes.

General Points

  • Decorators are called when the class is declared—not when an object is instantiated.
  • Multiple decorators can be defined on the same Class/Property/Method/Parameter.
  • Decorators are not allowed on constructors.

A valid decorator should be:

  1. Assignable to one of the Decorator types (ClassDecorator | PropertyDecorator | MethodDecorator | ParameterDecorator).
  2. Return a value (in the case of class decorators and method decorator) that is assignable to the decorated value.

Reference


Method / Formal Accessor Decorator

Implementation parameters:

  • target: The prototype of the class (Object).
  • propertyKey: The name of the method (string | symbol).
  • descriptor: A TypedPropertyDescriptor — If you’re unfamiliar with a descriptor’s keys, I would recommend reading about it in this documentation on Object.defineProperty (it’s the third parameter).

Example - Without Arguments

Use:

class MyClass { @log myMethod(arg: string) { return "Message -- " + arg; } } 

Implementation:

function log(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) { const originalMethod = descriptor.value; // save a reference to the original method // NOTE: Do not use arrow syntax here. Use a function expression in // order to use the correct value of `this` in this method (see notes below) descriptor.value = function(...args: any[]) { // pre console.log("The method args are: " + JSON.stringify(args)); // run and store result const result = originalMethod.apply(this, args); // post console.log("The return value is: " + result); // return the result of the original method (or modify it before returning) return result; }; return descriptor; } 

Input:

new MyClass().myMethod("testing"); 

Output:

The method args are: [“testing”]

The return value is: Message – testing

Notes:

  • Do not use arrow syntax when setting the descriptor’s value. The context of this will not be the instance’s if you do.
  • It’s better to modify the original descriptor than overwriting the current one by returning a new descriptor. This allows you to use multiple decorators that edit the descriptor without overwriting what another decorator did. Doing this allows you to use something like @enumerable(false) and @log at the same time (Example: Bad vs Good)
  • Useful: The type argument of TypedPropertyDescriptor can be used to restrict what method signatures (Method Example) or accessor signatures (Accessor Example) the decorator can be put on.

Example - With Arguments (Decorator Factory)

When using arguments, you must declare a function with the decorator’s parameters then return a function with the signature of the example without arguments.

class MyClass { @enumerable(false) get prop() { return true; } } function enumerable(isEnumerable: boolean) { return (target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) => { descriptor.enumerable = isEnumerable; return descriptor; }; } 

Static Method Decorator

Similar to a method decorator with some differences:

  • Its target parameter is the constructor function itself and not the prototype.
  • The descriptor is defined on the constructor function and not the prototype.

Class Decorator

@isTestable class MyClass {} 

Implementation parameter:

  • target: The class the decorator is declared on (TFunction extends Function).

Example use: Using the metadata api to store information on a class.


Property Decorator

class MyClass { @serialize name: string; } 

Implementation parameters:

  • target: The prototype of the class (Object).
  • propertyKey: The name of the property (string | symbol).

Example use: Creating a @serialize("serializedName") decorator and adding the property name to a list of properties to serialize.


Parameter Decorator

class MyClass { myMethod(@myDecorator myParameter: string) {} } 

Implementation parameters:

  • target: The prototype of the class (Function—it seems Function doesn’t work anymore. You should use any or Object here now in order to use the decorator within any class. Or specify the class type(s) you want to restrict it to)
  • propertyKey: The name of the method (string | symbol).
  • parameterIndex: The index of parameter in the list of the function’s parameters (number).

Simple example

Detailed Example(s)