Using TypeScript Decorators in Practise

TypeScript 5.0 Decorators

Chameera Dulanga
Bits and Pieces

--

Decorators are a design pattern widely used in programming to wrap something to change its behavior. Although this is a new concept for TypeScript programming languages like Python, Java, and C#Python, Java, and C# have already introduced this pattern before JavaScript. JavaScript decorators are still a stage 3 proposal and waiting for browser support.

However, TypeScript decided to shake everyone with the Typescript 5.0 update. They introduced some unique features in this update, including decorators to help developers apply logic to a class and its properties, methods, or parameters in a declarative manner at design time.

So, in this article, we’ll delve into TypeScript 5.0 decorators, comparing them with JavaScript counterparts and exploring their various types and practical use cases.

Decorators in JavaScript vs. TypeScript

Before we start with TypeScript decorators, you need to understand some significant differences between JavaScript and TypeScript decorators.

  • Extensive Decoration Capabilities: TypeScript decorators go beyond just classes and methods, allowing developers to annotate and modify various elements such as properties, accessors, and parameters.
  • Legacy Support for Parameter Decoration: Current TypeScript decorators don’t support decorating parameters. Hence, TypeScript has ensured backward compatibility for a smooth transition for developers. However, this will change soon.
  • Robust Type Checking: TypeScript rigorously checks the parameters and returns values of decorator functions as a strongly typed language.

TypeScript Decorators

In TypeScript, decorators are like special functions that can be attached to classes and their parts, like methods and properties. Based on this behavior, we can divide them into 5 main categories.

1. Class Decorators

Class decorators allow you to enhance or modify the behavior of a class. When you attach a function to a class as a decorator, you gain access to the class constructor as the first parameter.

For example, consider a Vehicle class and a class decorator that adds features related to navigation.

// Modified Class Decorator Function
function AddNavigationSystem(target: typeof Vehicle, context): typeof Vehicle {
if (context.kind === "class") {
// Add new properties 'currentLocation' and 'navigate' method
return class extends target {
currentLocation: string = "Unknown";

navigate(destination: string): void {
console.log(Navigating from ${this.currentLocation} to ${destination}`);

// Additional logic for navigation can be added here
this.currentLocation = destination;
}
};
}
}

// Applying the Modified Decorator
@AddNavigationSystem
class Vehicle {
// Other vehicle properties and methods
constructor(public brand: string) {}
}

// Creating an Instance of the Decorated Class
const navigableCar = new Vehicle("Toyota");

// Accessing the New Properties and Methods
console.log((navigableCar as any).currentLocation); // Prints: Unknown
(navigableCar as any).navigate("City Center");
console.log((navigableCar as any).currentLocation); // Prints: City Center

In this scenario, the class decorator AddNavigationSystem is applied to the Vehicle class. The decorator enhances the class by adding properties such as currentLocation and a method navigate for navigation purposes.

Also, the type of navigableCar is defined as any to access the new properties since decorators can’t influence the structure of the type.

2. Method Decorators

Method decorators come in handy when we desire certain actions to occur before or after the invocation of the decorated method. They can be used in situations like:

  • Logging method calls.
  • Verifying pre- or post-conditions.
  • Introducing delays,
  • Limiting the number of calls.
  • Marking a method as deprecated.
  • Issuing a warning message to users and suggesting an alternative method.

The code example below shows how to use method decorators to log method calls.

function logMethodCall(target: Function, context) {
if (context.kind === "method") {
return function (...args: any[]) {
console.log(Method ${context.name} is being called. );
return target.apply(this, args);
};
}
}

Here, the first parameter of the logMethodCall function represents the method being decorated. After confirming that it is indeed a method context.kind === "method", a new function is returned. This function acts as a wrapper around the decorated method, logging a message before executing the original method call.

Now, let’s apply this to a real world scenario like printing a document.

@PrintDecorations
class Printer {
// Other printer properties and methods
@logMethodCall
printDocument(document: string): void {
console.log(Printing document: ${document} );
}
}

const printer = new Printer();
printer.printDocument("Sample Document");

In the printDocument method, we use the logMethodCall decorator to log a message before executing the original method. Upon calling printDocument(), the output will display the log message:

Method printDocument is being called.
Printing document: Sample Document

3. Property Decorators

Property decorators share similarities with method decorators. For example, they can be employed to track accesses to a property or mark it as deprecated.

The below example shows an instance of marking a property as special using property decorators.

function specialProperty(_: any, context) {
if (context.kind === "field") {
return function (initialValue: any) {
console.log(${context.name} is a special property with initial value: ${initialValue} );
return initialValue;
};
}
}

class Product {
@specialProperty
productName: string;

constructor(productName: string) {
this.productName = productName;
}
}

const laptop = new Product("Laptop");
console.log(laptop.productName); // Prints: Laptop

In this example, the specialProperty decorator is used to mark the productName property as special. Upon creating an instance of the Product class and accessing the productName property, a message indicating its special nature will be logged:

productName is a special property with initial value: Laptop

4. Accessor Decorators

Accessor decorators are specifically designed to enhance or modify the behavior of getters and setters. They can be used in situations like:

  • Logging access — You can use accessor decorators to log when a getter or setter is accessed.
function logAccess(target, context) {
const kind = context.kind;
const logMessage = Accessed ${kind === "getter" ? "get" : "set"} for ${context.name}. ;

if (kind === "getter" || kind === "setter") {
return function (...args: any[]) {
console.log(logMessage);
return target.apply(this, args);
};
}
}
  • Validations and Transformations — You can validate or transform the value being set in a setter.
function validateValue(target, context) {
if (context.kind === "setter") {
return function (value: any) {
if (typeof value !== "number") {
throw new Error(Invalid value type for ${context.name}. );
}
// Additional validation or transformation logic can be added here
return target.call(this, value);
};
}
}
  • Deprecation Warnings — Similar to method and property decorators, accessor decorators can be used to mark getters or setters as deprecated.
function deprecated(target, context) {
const kind = context.kind;
const msg = ${context.name} is deprecated and will be removed in a future version. ;

if (kind === "getter" || kind === "setter") {
return function (...args: any[]) {
console.log(msg);
return target.apply(this, args);
};
}
}
  • Initialization Logic — Accessor decorators can be used to add initialization logic before or after a getter or setter is invoked.
function initializeProperty(target, context) {
if (context.kind === "setter") {
return function (value: any) {
// Initialization logic before setting the value
console.log(Initializing ${context.name}. );
return target.call(this, value);
};
}
}

5. Auto-accessor Decorators

The auto-accessor field is introduced in the new decorator proposal for TypeScript. When you declare a class field with the accessor keyword, the transpiler automatically generates a pair of getter and setter methods and a private property behind the scenes.

This feature simplifies the representation of a basic accessor pair and addresses potential issues that could arise when applying decorators directly to class fields.

Here’s an example of using the auto-accessor field in TypeScript:

class Test {
accessor x: number;
}

TypeScript’s transpiler will transform the x field into the equivalent of the following:

class Test {
private _x: number;

get x(): number {
return this._x;
}

set x(value: number) {
this._x = value;
}
}

This automatic generation of getter and setter methods, backed by private property, enhances the consistency and reliability of applying decorators to class fields.

Conclusion

TypeScript decorators offer a powerful mechanism to enhance and modify the behavior of classes and their components. From class decorators that enable the augmentation of entire classes to method, property, and accessor decorators that allow fine-grained customization, TypeScript decorators provide developers with increased flexibility and expressiveness in their code.

Whether you’re a seasoned developer or just getting started, embracing and mastering TypeScript decorators opens up new avenues for creating robust and scalable applications. So, remember to harness the power of TypeScript decorators for your projects to elevate your coding endeavors. Happy coding!

--

--