- Published on
Mastering TypeScript Decorators A Comprehensive Guide
- Authors
- Name
- Yinhuan Yuan
Introduction
TypeScript decorators are a powerful feature that allows you to add both annotations and metadata to existing code. They provide a way to modify classes and properties at design time. In this blog post, we'll explore what decorators are, how to use them, and some practical examples of their application.
- What are Decorators?
- Enabling Decorators
- Types of Decorators
- Decorator Factories
- Decorator Composition
- Practical Use Cases
- Conclusion
What are Decorators?
Decorators are a way to add new functionality to classes, methods, properties, or parameters without modifying their original code. They are a form of metaprogramming, allowing you to modify or enhance the behavior of code at design time.
In TypeScript, a decorator is simply a function that is called with specific arguments depending on where it is applied.
Enabling Decorators
To use decorators in TypeScript, you need to enable them in your tsconfig.json
file:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
Types of Decorators
TypeScript supports five types of decorators:
- Class Decorators
- Method Decorators
- Property Decorators
- Accessor Decorators
- Parameter Decorators
Let's explore each type with examples.
1. Class Decorators
Class decorators are applied to the constructor of the class and can be used to observe, modify, or replace a class definition.
function sealed(constructor: Function) {
Object.seal(constructor)
Object.seal(constructor.prototype)
}
@sealed
class Greeter {
greeting: string
constructor(message: string) {
this.greeting = message
}
greet() {
return 'Hello, ' + this.greeting
}
}
In this example, the @sealed
decorator seals both the constructor and its prototype, preventing new properties from being added to them.
2. Method Decorators
Method decorators are applied to methods within a class. They can be used to observe, modify, or replace a method definition.
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value
}
}
class Greeter {
greeting: string
constructor(message: string) {
this.greeting = message
}
@enumerable(false)
greet() {
return 'Hello, ' + this.greeting
}
}
Here, the @enumerable(false)
decorator makes the greet
method non-enumerable.
3. Property Decorators
Property decorators are used to observe, modify, or replace a property definition.
function format(formatString: string) {
return function (target: any, propertyKey: string): any {
let value = target[propertyKey]
const getter = function () {
return `${formatString} ${value}`
}
const setter = function (newVal: string) {
value = newVal
}
return {
get: getter,
set: setter,
enumerable: true,
configurable: true,
}
}
}
class Greeter {
@format('Hello,')
greeting: string
}
const greeter = new Greeter()
greeter.greeting = 'World'
console.log(greeter.greeting) // Outputs: "Hello, World"
In this example, the @format
decorator prefixes the greeting
property with "Hello,".
4. Accessor Decorators
Accessor decorators are applied to the accessor of a property (getter or setter). They can be used to observe, modify, or replace an accessor's definitions.
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value
}
}
class Point {
private _x: number
private _y: number
constructor(x: number, y: number) {
this._x = x
this._y = y
}
@configurable(false)
get x() {
return this._x
}
@configurable(false)
get y() {
return this._y
}
}
Here, the @configurable(false)
decorator makes the accessors non-configurable.
5. Parameter Decorators
Parameter decorators are applied to the parameters of a method or constructor. They can be used to observe the parameters.
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let existingRequiredParameters: number[] =
Reflect.getOwnMetadata('required', target, propertyKey) || []
existingRequiredParameters.push(parameterIndex)
Reflect.defineMetadata('required', existingRequiredParameters, target, propertyKey)
}
class Greeter {
greet(@required name: string) {
return 'Hello ' + name
}
}
In this example, the @required
decorator marks the parameter as required. Note that this example uses reflect-metadata
, which needs to be imported and configured separately.
Decorator Factories
A decorator factory is simply a function that returns the expression that will be called by the decorator at runtime. It allows you to customize how a decorator is applied to a declaration.
function color(value: string) {
return function (target) {
// This is the decorator factory
return class extends target {
color = value
}
}
}
@color('red')
class Car {
constructor(
public make: string,
public model: string
) {}
}
let car = new Car('Toyota', 'Corolla')
console.log(car.color) // Outputs: "red"
Decorator Composition
Multiple decorators can be applied to a declaration. They are evaluated in the order they appear in the code, from top to bottom.
function first() {
console.log('first(): factory evaluated')
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('first(): called')
}
}
function second() {
console.log('second(): factory evaluated')
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('second(): called')
}
}
class ExampleClass {
@first()
@second()
method() {}
}
This will output:
first(): factory evaluated
second(): factory evaluated
second(): called
first(): called
Practical Use Cases
- Memoization: Caching the results of expensive computations.
- Logging: Automatically logging method calls or property access.
- Validation: Ensuring that parameters or properties meet certain criteria.
- Authorization: Checking if a user has permission to call a method.
- Lazy Loading: Deferring the initialization of a property until it's first accessed.
Conclusion
Decorators are a powerful feature in TypeScript that enable you to write more expressive and maintainable code. They provide a clean way to modify or enhance the behavior of your classes, methods, properties, and parameters.
While decorators are still technically an experimental feature in TypeScript, they are widely used in many popular frameworks and libraries, such as Angular and MobX.
As you start using decorators in your TypeScript projects, remember that with great power comes great responsibility. Use them judiciously to enhance your code without making it overly complex or hard to understand.
Happy coding with TypeScript decorators