Using decorators in TypeScript

A comprehensive guide for devs interested in using TypeScript decorators. Covers types, logging, memorization. validation, and advanced topics.

Content

Apify's mission is to make the web more programmable. Web scraping for data is a big part of that. This run-through of TypeScript decorators was inspired by the many ways of applying TS in web scraping. BTW, we have some great TypeScript templates if you want to make scrapers in TS.

The concept of decorators is not new to programming. So what makes them different in TypeScript? In simple terms, in TypeScript, a decorator is a special kind of declaration that can be attached to a class declaration, method, property, or parameter declaration. It is denoted by an @ symbol followed by the decorator's name.

TS decorators are a powerful feature used to modify the behavior of classes, methods, properties, or parameters in TypeScript. They are similar to other decorators in concept, but the syntax and usage are made to fit TypeScript specifically. Let's zoom in on the types, specificities, advanced topics, and use cases of TS decorators.

🤔 What are decorators in TypeScript?

The original decorators were mostly designed based on Java, called "annotations" instead. Decorators in TypeScript came about as a feature borrowed from languages like Java, but also Python and C#. Decorators are a type of expression that can be attached to many kinds of elements, acting as a kind of annotation or modification that can alter the behavior of the element they're attached to.

Essentially, at their core, decorators are functions that return a function, which allows them to perform their modifications when the decorated elements are invoked. Using decorators helps to make for a more readable, declarative programming experience. They're often used for metaprogramming tasks such as logging, memoization, or auto-binding, providing a clean and efficient way to extend the functionality of TypeScript code without altering the underlying logic of the decorated elements. We'll be exploring the use cases for these further down.

⚡️ Decorators in TypeScript vs. JavaScript

Before we look at specifics, let's examine some of the main differences between decorators in JavaScript and TypeScript. As you can see, the main difference is that JS has limited capacities for those:

Feature JavaScript decorators TypeScript decorators
Decoratable entities Class declarations, methods Class declarations, methods, properties, accessors, parameters (upcoming)
Parameter decoration (as of now) Not supported Supported (old decorators)
Type checking No Yes

Let's analyze this table in a bit more detail:

  1. Decoratable entities. In JavaScript, decorators are mainly used for class declarations and methods. In TypeScript, decorators can be applied to class declarations, methods, properties, accessors (getters and setters), and will also support parameters (in older decorator syntax).
  2. Parameter decoration. As of now, JavaScript decorators do not support parameter decoration. However, in TypeScript, older decorators do support parameter decoration, and the support for parameters is planned to be available in the new decorator syntax as well.
  3. Type checking. Since TypeScript is a statically typed language, it can perform type checking and validation on the parameters and return values of decorator functions. JavaScript, being dynamically typed, lacks this type-checking capability and relies on runtime checks or external tools like linters to catch type errors.
TypeScript vs. JavaScript for web scraping
Is TypeScript better than JavaScript?

Read more about the main differences between TypeScript and JavaScript for web scraping

🛠 Installation

Before you can start using decorators in TypeScript, you need to set up your development environment to support them. Here's a step-by-step guide on how to enable decorator support:

1️⃣ Install TypeScript. If you haven't already, you'll need to install TypeScript globally on your system. You can do this using npm (Node Package Manager) by running the following command:

npm install -g typescript

This command installs TypeScript globally, allowing you to use it from the command line.

2️⃣ Initialize a TypeScript project. If you're starting a new project, navigate to your project's directory and run the following command to create a tsconfig.json file:

tsc --init

This command generates a TypeScript configuration file that you can customize to suit your project's needs.

3️⃣ Enable experimentalDecorators. Open the tsconfig.json file in a text editor, and locate the "compilerOptions" section. Within that section, add the "experimentalDecorators" option and set it to true:

"compilerOptions": {
    // ...
    "experimentalDecorators": true,
    // ...
}

This configuration option enables TypeScript's experimental decorator support.

4️⃣ Install required dependencies. Depending on your project's needs, you may need to install additional packages and dependencies for specific decorators or libraries you plan to use. For example, if you're working with decorators in a popular framework like Angular, you'll need to follow the framework's installation instructions.

With these steps completed, your TypeScript environment should be ready to use decorators in your code.

⚗️ How to use decorators in TypeScript

Decorators in TypeScript offer a wide range of use cases to enhance your codebase. Here are some common scenarios in which you can use decorators:

  • Logging. Decorators can be used to log method calls, parameter values, or property changes. This is particularly useful for debugging and monitoring application behavior.
function logMethod(target: any, methodName: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`Calling method ${methodName} with arguments: ${args}`);
        return originalMethod.apply(this, args);
    };
}

class Example {
    @logMethod
    someMethod() {
        // Method implementation...
    }
}
  • Validation. You can use decorators to validate input parameters or properties. This is valuable for ensuring that data adheres to specific rules or constraints.
function validateParameter(target: any, methodName: string, parameterIndex: number) {
    const originalMethod = target[methodName];
    target[methodName] = function (...args: any[]) {
        const paramValue = args[parameterIndex];
        if (typeof paramValue !== 'number' || isNaN(paramValue)) {
            throw new Error(`Invalid parameter at index ${parameterIndex}`);
        }
        return originalMethod.apply(this, args);
    };
}

class Example {
    someMethod(@validateParameter value: number) {
        // Method implementation...
    }
}

Dependency injection. Many frameworks, such as Angular, utilize decorators for dependency injection. You can use decorators to specify the dependencies of a class or component.

import { Injectable } from '@angular/core';

@Injectable()
class MyService {
    // Service implementation...
}
  • Route handling (Angular/Express). In web applications, decorators can be used to define routing information. For example, in Angular or Express.js, decorators can be applied to components or controllers to specify routes and request-handling logic.
@Component({
    selector: 'app-my-component',
    templateUrl: './my-component.component.html',
})
@Route('/my-route')
export class MyComponent {
    // Component logic...
}
  • Memoization. Last but not least, decorators can be used to cache the results of function calls, improving performance by avoiding redundant computations.
function memoize(target: any, propertyName: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const cache = new Map();

    descriptor.value = function (...args: any[]) {
        const key = JSON.stringify(args);
        if (cache.has(key)) {
            return cache.get(key);
        }
        const result = originalMethod.apply(this, args);
        cache.set(key, result);
        return result;
    };
}

class Example {
    @memoize
    expensiveOperation(x: number, y: number) {
        // Expensive operation...
        return x + y;
    }
}

These are just a few examples of how decorators can be used in TypeScript to enhance your code. Their versatility and ability to add cross-cutting concerns make them a valuable tool for improving code maintainability and organization.

🗂 What types of decorators are there in TypeScript?

  1. Class decorators.

Class decorators are applied to classes and can be used to modify class behavior. They are declared just before the class definition and receive the constructor of the class as their target.

function classDecorator(constructor: Function) {
    console.log("Class decorator executed.");
}

@classDecorator
class MyClass {
    // Class implementation...
}

2. Method decorators.

Method decorators are applied to methods within a class and are declared just before the method definition. They receive three parameters: the target (prototype of the class), the method name, and a property descriptor for the method.

function methodDecorator(target: any, methodName: string, descriptor: PropertyDescriptor) {
    console.log("Method decorator executed for method:", methodName);
}

class MyClass {
    @methodDecorator
    myMethod() {
        // Method implementation...
    }
}

3. Property decorators.

Property decorators are applied to class properties and are declared just before the property definition. They receive two parameters: the target (prototype of the class) and the property name.

function propertyDecorator(target: any, propertyName: string) {
    console.log("Property decorator executed for property:", propertyName);
}

class MyClass {
    @propertyDecorator
    myProperty: string;
}

4. Accessor and auto-accessor decorators.

This type is used to modify the behavior of getter and setter methods in TypeScript. Accessor decorators are declared just before the getter or setter definition and can be helpful for tasks like validation, logging, or implementing computed properties. Let's look at an example of both accessor and auto-accessor decorators:

// Accessor Decorator
function logGetter(target: any, propertyKey: string) {
    const getter = Object.getOwnPropertyDescriptor(target, propertyKey)?.get;
    if (getter) {
        Object.defineProperty(target, propertyKey, {
            get: function () {
                console.log(`Getting ${propertyKey}`);
                return getter.call(this);
            },
        });
    }
}

// Auto-Accessor Decorator
function uppercaseSetter(target: any, propertyKey: string) {
    const setter = Object.getOwnPropertyDescriptor(target, propertyKey)?.set;
    if (setter) {
        Object.defineProperty(target, propertyKey, {
            set: function (value: string) {
                console.log(`Setting ${propertyKey} to: ${value}`);
                setter.call(this, value.toUpperCase());
            },
        });
    }
}

class Person {
    private _name: string = '';

    @logGetter
    get name(): string {
        return this._name;
    }

    @uppercaseSetter
    set name(value: string) {
        this._name = value;
    }
}

const person = new Person();

person.name = 'John Doe'; // Setting name to: John Doe
console.log(person.name); // Getting name
console.log(person.name); // JOHN DOE

5. Parameter decorators.

⚠️
Warning: old TypeScript decorators.

Parameter decorators are applied to the parameters of a method or constructor and are declared just before the parameter declaration. They receive three parameters: the target (prototype of the class), the method name (or constructor), and the parameter index.

function parameterDecorator(target: any, methodName: string, parameterIndex: number) {
    console.log("Parameter decorator executed for method:", methodName, "parameter index:", parameterIndex);
}

class MyClass {
    myMethod(@parameterDecorator parameter: string) {
        // Method implementation...
    }
}

It's worth noting that decorators in TypeScript are experimental and part of the ECMAScript proposal. Therefore, you may need to enable the experimentalDecorators compiler option in your TypeScript configuration to use them. Additionally, decorators are widely used in libraries and frameworks like Angular to provide features like dependency injection, logging, and more.

🎖 Advanced topics

Decorator factories

Decorator factories are functions that return the expression that will be called by the decorator at runtime. This concept allows for greater customization of how decorators are applied to declarations. By using decorator factories, you can create decorators with parameters, making them more flexible and versatile.

Here's an example of a decorator factory:

function customDecorator(value: string) {
    return function (target: any, propertyName: string) {
        console.log(`Custom decorator executed for property ${propertyName} with value ${value}`);
    };
}

class MyClass {
    @customDecorator("Hello, world!")
    myProperty: string;
}

In this example, customDecorator is a decorator factory that takes a value parameter and returns a decorator function. This decorator can now be applied to class properties with different values, providing a way to configure the behavior of the decorator at runtime.

Evaluation order

When multiple decorators are applied to a single declaration, understanding their evaluation order is crucial. Decorators are executed in a bottom-up order, meaning the innermost decorator is called first, followed by the outer ones. This allows for composability and layering of behavior.

function firstDecorator(target: any) {
    console.log("First decorator");
}

function secondDecorator(target: any) {
    console.log("Second decorator");
}

@firstDecorator
@secondDecorator
class MyClass {
    // Class implementation...
}

In this example, "Second decorator" will be logged before "First decorator" because the @secondDecorator is the innermost decorator.

Metadata

Decorators in TypeScript can be used to emit and read metadata associated with class declarations and members. TypeScript provides a Reflect object that can be used to access this metadata.

For example, you can use the Reflect.metadata method to add metadata to a class property:

class MyClass {
    @Reflect.metadata("customKey", "customValue")
    myProperty: string;
}

// Accessing the metadata
const metadataValue = Reflect.getMetadata("customKey", MyClass.prototype, "myProperty");
console.log("Metadata:", metadataValue); // Output: Metadata: customValue

In this example, we add metadata "customValue" to the myProperty using Reflect.metadata, and later, we retrieve it using Reflect.getMetadata.

☕️ TypeScript decorators vs. decorators in other languages

TypeScript decorators vs. Python decorators

TypeScript decorators share similarities with Python decorators in that they both allow you to modify the behavior of functions or methods. However, TypeScript decorators have a broader scope, as they can also be applied to class declarations, properties, accessors, and parameters, while Python decorators are typically used with functions and methods.

TypeScript decorators vs. Java annotations

TypeScript decorators draw inspiration from Java annotations. Both TypeScript decorators and Java annotations serve as metadata that can modify the behavior of classes or methods. However, there are key differences. TypeScript decorators are more flexible, allowing you to modify a wider range of elements (properties, accessors, parameters) and supporting runtime execution. Java annotations, on the other hand, are primarily compile-time constructs and do not have the same level of runtime flexibility.

Unique suitability to TypeScript

TypeScript decorators are uniquely suited to the language's static typing and object-oriented features. They provide a way to enhance and extend TypeScript code while maintaining strong type checking. This makes them particularly appealing to developers who are familiar with statically typed languages and object-oriented programming paradigms.

Strengths of TypeScript decorators

The strengths of TypeScript decorators lie in their ability to modify class declarations and members. They enable developers to add cross-cutting concerns such as logging, validation, and dependency injection to their codebase in a clean and modular way. Additionally, the ability to create decorator factories adds a level of customization that can be tailored to specific use cases, making decorators a powerful tool for extending TypeScript applications.

TypeScript decorators are a powerful feature that adds a new dimension of flexibility and modularity to TypeScript code. They make it easier for devs to implement cross-cutting concerns and enhance code maintainability.

To learn more about decorators, check out the official TypeScript documentation.

❓FAQ

Are decorators supported in browsers?

As of October 2023, most browsers do not support decorators. You can still test them out by using compilers like Babel.

Browser Compatibility Score for TS decorators
Browser Compatibility Score for decorators

How do you enable decorator support in TypeScript?

To enable decorator support in TypeScript, you need to set the "experimentalDecorators" option to true in your TypeScript configuration (tsconfig.json).

What are the limitations of decorators in TypeScript?

Some limitations of decorators in TypeScript include limited support in browsers, as most do not natively support decorators. Additionally, decorator support is still considered experimental and may evolve in future TypeScript versions.

How do you create custom decorators in TypeScript?

You can create custom decorators in TypeScript by defining a function that returns the decorator logic. This function can accept parameters to customize the decorator's behavior.

How do you use third-party libraries that provide decorators for TypeScript?

To use third-party libraries with decorators in TypeScript, you typically need to install the library and follow the usage instructions provided by the library's documentation.

How do you test code that uses decorators in TypeScript?

You can test code that uses decorators in TypeScript like any other code. Use testing frameworks and tools such as Jasmine, Jest, or Mocha to write unit tests for your decorated classes and methods. Ensure that your testing setup includes support for TypeScript decorators.

📕 Resources

Natasha Lekh
Natasha Lekh
Crafting content that charms both readers and Google’s algorithms: readmes, blogs, and SEO secrets.

Get started now

Step up your web scraping and automation