loading...

Introduction to TypeScript Property Decorators

coryrylan profile image Cory Rylan Originally published at coryrylan.com on ・3 min read

Decorators are a language construct that allows us to add additional behavior to a Class. Decorators are in many languages, and in this post, we will learn how to create and use a custom Class Property Decorators in TypeScript.

TypeScript supports various kinds of decorators, including Class, Method, and Property Decorators. Decorators are a language feature proposal for JavaScript, which you can find in the TC39 Documentation. Decorators use the @ character to signify a decorator call.

import { id } from './id.decorator';

export class Component {
  @id() instanceId: string;

  log() {
    console.log('id', this.instanceId);
  }
}

In this example, we have created a @id decorator that will create a simple random id that we can use in my components. Decorators are JavaScript functions that are executed once for the class definition and not per the instance.

First, for our @id decorator, we need to create a function to generate the ID values for us. To correctly make truly unique IDs, we would need to use a library or use the newer browser crypto APIs. For simplicity of the demo, we will generate using a function that will be unique enough for our client-side use cases.

// Read more about the UUID proposal here https://github.com/tc39/proposal-uuid
export function createId() {
  return Math.random().toString(36).substr(2, 9);
}

Decorators are JavaScript functions. Decorators can also have parameters by returning an inner function. When a Property Decorator is executed at runtime, the prototype is passed as a target reference. A second parameter is passed as the name of the property that the Decorator is placed on.

export function id() {
  return (target: {} | any, name?: PropertyKey): any => {
    const descriptor = {
      get(this: any) { },
      set(value: any) { },
      enumerable: true,
      configurable: true,
    };

    Object.defineProperty(target, name, descriptor);
  };
}

With the target, we can define a descriptor. The descriptor allows us to define a new getter and setter for the Decorator. With our custom getter and setter, we can apply our custom logic for our Decorator.

export function id() {
  return (target: {} | any, name: PropertyKey): any => {
    const descriptor = {
      get(this: any) {
        const propertyName = `__${String(name)}`;

        if (!this[propertyName]) {
          this[propertyName] = createId();
        }

        return this[propertyName];
      },
      enumerable: true,
      configurable: true,
    };

    Object.defineProperty(target, name, descriptor);
  };
}

When the getter is called, we receive a reference to the Class instance. With this reference, we can create a backing field to hold the unique ID for when the next time the getter is called. We prefix the backing field with a double underscore (dunder) so that we don’t cause a collision with any existing fields/properties.

Now with our Decorator we can create IDs for components.

export class Component {
  @id() instanceId: string;

  @id() secondaryId: string;

  log() {
    console.log(this.instanceId, this.secondaryId);
  }
}

const component = new Component();
const component2 = new Component();

// Each instance is unique and each property within the instance is also unique
component.log(); // _115fl2ygf _jwlv4b9dc
component2.log(); // _ql8hudynl _7eqg80p64

Decorators in Libraries

With Decorators still in the proposal stage, we have to add a bit more work if wewant to ship the decorators as part of a library. If a consumer of our Decorator were to use Babel or eventually the native implementation, we will need to make sure we follow the appropriate API.

When the Decorator is executed, we need to handle the different signatures.

export function id() {
  return (protoOrDescriptor: {} | any, name?: PropertyKey): any => {
    const descriptor = {
      get(this: any) {
        const propertyName = name !== undefined ? `__${String(name)}` : `__${protoOrDescriptor.key}`;

        if (!this[propertyName]) {
          this[propertyName] = createId();
        }

        return this[propertyName];
      },
      enumerable: true,
      configurable: true,
    };

    // if there is no name then this is a TypeScript runtime else its the current native TC39 proposal
    return name !== undefined
      ? legacyId(descriptor, protoOrDescriptor as {}, name)
      : standardId(descriptor, protoOrDescriptor as any);
  };
}

// Current TS API
const legacyId = (descriptor: PropertyDescriptor, proto: {}, name: PropertyKey) => {
  Object.defineProperty(proto, name, descriptor);
};

// TC39 Decorators proposal
const standardId = (descriptor: PropertyDescriptor, element: any) => ({
  kind: 'property',
  placement: 'prototype',
  key: element.key,
  descriptor,
});

Decorators in TypeScript have been around for a while, but if you are using plain JavaScript, be aware the Decorators proposal is still not 100% standardized, so it is subject to change or never be implemented. If you are developing an application, this may not be an issue using Decorators with TypeScript or Babel. However, if you are a library author, be cautious of shipping decorators as part of the public API.

Check out the full working demo!

Posted on by:

coryrylan profile

Cory Rylan

@coryrylan

My name is Cory Rylan. Google Developer Expert and Front End Software Developer for VMware Clarity. Angular Boot Camp instructor.

Discussion

pic
Editor guide