This repository contains examples of Typescript decorators.
Typescript decorators are a way to add metadata to classes, methods, properties, or parameters. They are a feature of the language that allows you to write code that modifies the behavior of a class or method. Decorators are a way to add functionality to a class or method without changing the class or method itself.
A typescript is essentially a function that takes a target object and a few metadata.
function myDecorator(target: any, /* other info */) {
/* do stuff */
}
@myDecorator
/* the class, property or method */
Example:
function myDecorator<T extends Object>(target: T) {
console.log(`Decorating ${target.constructor.name}`);
}
@myDecorator
class MyClass {
/* ... */
}
// Output: Decorating MyClass
You can apply multiple decorators to the same class, property, or method.
@decorator1
@decorator2
@decorator3
/* the class, property or method */
A decorator factory is a way to pass parameters to a decorator. It's a function that returns a decorator (another function).
function myDecoratorFactory(/* decorator parameters */) {
return function myDecorator(target: any, /* other info */) {
/* do stuff */
};
}
Usage:
@myDecoratorFactory(/* decorator parameters */)
/* the class, property or method */
One of the primary use cases for decorators is to add metadata to a class, method, property, or parameter. This metadata can be used by other parts of the code to determine how to interact with the decorated element.
Typescript provides a way to access this metadata using the Reflect
object. The Reflect
object is available through a polyfill using the library reflect-metadata
.
import 'reflect-metadata';
function myDecorator(target: any) {
Reflect.defineMetadata('myMetadata', 'myValue', target);
}
@myDecorator
class MyClass {
/* ... */
}
const metadata = Reflect.getMetadata('myMetadata', MyClass);
There are five types of decorators in Typescript:
- Class decorators
- Method decorators
- Accessor decorators
- Property decorators
- Parameter decorators
Type Signature:
/** @param {T} constructor the class constructor */
function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T): void | T {
/* do stuff with constructor */
/* ... */
/* eventually return a new constructor */
// return class extends constructor {
// /* do stuff with the new constructor */
// };
}
Usage:
@classDecorator
class MyClass {
/* ... */
}
Examples:
- Class Sealing: A class decorator that seals a class.
- Class Registry: A class decorator that adds a class to a registry (a key-value map).
- Class Reference Count: A class decorator that rewrites a class constructor to keep track of the number of instances created.
- Class Metadata: A class decorator that adds some metadata to a class that can be retrieved later.
- Class Singleton: A class decorator that makes a class a singleton.
- Class Method Logging: A class decorator that logs every method call by wrapping them.
Caveats
- When re-writing a class (like in Class Method Logging), you need to be aware that the new class will have a different name and prototype than the original class.
- Take care of the typing: if you extend the original class, the new methods won't be visible in the type system.
- In the example Class Registry, be aware that Tree-shaking may remove the class from the registry if it's not used anywhere else in the code. You may need to explicitly import the whole module in the entry file to prevent this.
Type Signature:
/** @param {T} target the class prototype */
/** @param {string} propertyKey the name of the method */
/** @param {PropertyDescriptor} descriptor the method descriptor */
function methodDecorator<T extends Object>(target: T, propertyKey: string, descriptor: PropertyDescriptor) {
/* do stuff with target, propertyKey, and descriptor */
/* ... */
/* eventyally modify the descriptor */
// descriptor.value = function () {
// /* do stuff with the original method */
// /* ... */
// };
}
Usage:
class MyClass {
@methodDecorator
myMethod() {
/* ... */
}
}
Examples:
- Method Typecheck: A method decorator that type-checks the arguments and return value of a method.
- Method Logging: A method decorator that wraps a method to catch and log errors that occur during its execution.
- Method Timeout: A method decorator that introduces a timeout to an async method.
Signature:
/** @param {T} target the class prototype */
/** @param {string} propertyKey the name of the accessor */
/** @param {PropertyDescriptor} descriptor the accessor descriptor */
function accessorDecorator<T extends Object>(target: T, propertyKey: string, descriptor: PropertyDescriptor) {
/* do stuff with target, propertyKey, and descriptor */
/* ... */
/* eventyally modify the descriptor */
// descriptor.get = function () {
// /* do stuff with the original getter */
// /* ... */
// };
}
Usage:
class MyClass {
@accessorDecorator
get myProperty() {
/* ... */
}
}
Examples:
- Accessor Cache: An accessor decorator that caches the result of a getter. It also show how to invalidate the cache.
- Accessor Descriptors: An accessor decorator that interacts with the property descriptor of a getter and changes its behavior. (i.e. making it read-only, enumerable, etc.)
- Accessor Logging: An accessor decorator that logs the access to a getter.
Signature:
/** @param {T} target the class prototype */
/** @param {string} propertyKey the name of the property */
function propertyDecorator<T extends Object>(target: T, propertyKey: string) {
/* do stuff with target and propertyKey */
/* ... */
}
Usage:
class MyClass {
@propertyDecorator
myProperty: string;
}
Examples:
- Property Dependency Injection: A property decorator that injects a dependency into a property.
- Property Metadata Typecheck: A property decorator that adds typing metadata to a property, then checks it using a function.
- Property Angular-like Input: A property decorator that creates an Angular-like input property.
Pro Tip:
The example Property Metadata Typecheck can be achieved with no code by setting the emitDecoratorMetadata
option to true
in the tsconfig.json
file.
{
"compilerOptions": {
"emitDecoratorMetadata": true
}
}
Caveats
In the Property Dependency Injection
example, may not work if you set the target to ES2022
in the tsconfig.json
file. You may need to set useDefineForClassFields
to false
to make it work.
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": false
}
}
This is because the useDefineForClassFields
option changes the way class fields are defined.
Signature:
/** @param {T} target the class prototype */
/** @param {string} propertyKey the name of the method */
/** @param {number} parameterIndex the index of the parameter */
function parameterDecorator<T extends Object>(target: T, propertyKey: string, parameterIndex: number) {
/* do stuff with target, propertyKey, and parameterIndex */
/* ... */
}
Usage:
class MyClass {
myMethod(@parameterDecorator myParameter: string) {
/* ... */
}
}
Caveats
Parameter decorators can't modify directly the behavior of a method. They can only be used to add metadata to a parameter. You may need to use them in conjunction with other decorators to achieve the desired behavior. See the examples below.
Examples:
- Parameter Validation: Parameter validation using parameter decorators.
- Parameter Dependency Injection: Shows how to achieve an angular-like dependency injection using parameter decorators in the constructor.
- Parameter Transformation: Shows how to transform or sanitize parameters using parameter decorators.
- Parameter Can't Modify Methods: Shows that parameter decorators can't modify the behavior of a method directly.
Unfortunately Typescript decorators unfortunately don't support decorating a function directly.
function myDecorator() {
/* ... */
}
// This won't work
@myDecorator
function myFunction() {
}
However, you can achieve similar functionality by using higher-order functions.
function myDecorator(fn: Function) {
return function (...args: any[]) {
/* do stuff before calling the function */
/* ... */
const result = fn(...args);
/* do stuff after calling the function */
/* ... */
return result;
};
}
const myFunction = myDecorator(function () {
/* ... */
});
Examples:
- Function decorator: Shows how to achieve similar functionality to a function decorator using higher-order functions.
- Function fancier decorator: Shows a pattern that helps achieving more complex functionality by introducing a
decorate
function.