Quick Tip: Decorators in TypeScript

    Steve Kinney
    Share

    In this quick tip, excerpted from Unleashing the Power of TypeScript, Steve shows you how to use decorators in TypeScript, which is a new feature in TypeScript 5.

    Decorators have almost been part of ECMAScript for as long as I can remember. These nifty tools let us modify classes and members in a reusable way. They’ve been on the scene for a while in TypeScript — albeit under an experimental flag. Although the Stage 2 iteration of decorators was always experimental, decorators have been widely used in libraries like MobX, Angular, Nest, and TypeORM. TypeScript 5.0’s decorators are fully in sync with the ECMAScript proposal, which is pretty much ready for prime time, sitting at Stage 3.

    Decorators let us craft a function that tweaks the behavior of a class and its methods. Imagine needing to sneak in some debug statements into our methods. Before TypeScript 5.0, we’d have been stuck copying and pasting the debug statements manually in each method. With decorators, we just do the job once and the change will be supported through each method the decorator is attached to.

    Let’s say we want to create a decorator for logging that a given method is deprecated:

    class Card {
      constructor(public suit: Suit, public rank: Rank) {
        this.suit = suit;
        this.rank = rank;
      }
    
      get name(): CardName {
        return `${this.rank} of ${this.suit}`;
      }
    
      @deprecated // 👀 This is a decorator!
      getValue(): number {
        if (this.rank === 'Ace') return 14;
        if (this.rank === 'King') return 13;
        if (this.rank === 'Queen') return 12;
        if (this.rank === 'Jack') return 11;
        return this.rank;
      }
    
      // The new way to do it!
      get value(): number {
        if (this.rank === 'Ace') return 14;
        if (this.rank === 'King') return 13;
        if (this.rank === 'Queen') return 12;
        if (this.rank === 'Jack') return 11;
        return this.rank;
      }
    }
    
    const card = new Card('Spades', 'Queen');
    card.getValue();
    

    We want a warning message logged to the console whenever card.getValue() is called. We could implement the above decorator as follows:

    const deprecated = <This, Arguments extends any[], ReturnValue>(
      target: (this: This, ...args: Arguments) => ReturnValue,
      context: ClassMethodDecoratorContext<
        This,
        (this: This, ...args: Arguments) => ReturnValue
      >,
    ) => {
      const methodName = String(context.name);
    
      function replacementMethod(this: This, ...args: Arguments): ReturnValue {
        console.warn(`Warning: '${methodName}' is deprecated.`);
        return target.call(this, ...args);
      }
    
      return replacementMethod;
    };
    

    This might look a little confusing at first, but let’s break it down:

    • Our decorator function takes two arguments: target and context.
    • target is the method itself that we’re decorating.
    • context is metadata about the method.
    • We return some method that has the same signature.
    • In this case, we’re calling console.warn to log a deprecation notice and then we’re calling the method.

    The ClassMethodDecorator type has the following properties on it:

    • kind: the type of the decorated property. In the example above, this will be method, since we’re decorating a method on an instance of a Card.
    • name: the name of property. In the example above, this is getValue.
    • static: a value indicating whether the class element is a static (true) or instance (false) element.
    • private: a value indicating whether the class element has a private name.
    • access: an object that can be used to access the current value of the class element at runtime.
    • has: determines whether an object has a property with the same name as the decorated element.
    • get: invokes the setter on the provided object.

    You can kick the tires of the code samples above in this playground.

    Decorators provide convenient syntactic sugar for adding log messages — like we did in the example above — as well as a number of other common use cases. For example, we could create a decorator that automatically binds the method to the current instance or that modifies the property descriptor of the method or class.

    This article is excerpted from Unleashing the Power of TypeScript, available on SitePoint Premium and from ebook retailers.