Typescript 5 decorators

TypeScript five has just been released. In this release, TypeScript has implemented the new upcoming ECMA script decorators standard. Let’s take a look.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@kreuzercode

03.04.2023

6 min read

Typescript 5 decorators
share

TypeScript five has just been released. In this release, TypeScript has implemented the new upcoming ECMA script decorators standard. Let’s take a look.

Wait a minute? Upcoming decorator standard? I have been using TypeScript decorators for years. How was this working so far if the standard isn’t here yet?

--experimentalDecorators

TypeScript has supported experimental decorators for a while, but for it to work, it required the use of a compiler flag called --experimentalDecorators.

With the latest update, the flag is no longer necessary, and decorators can be used without it.

But hold on; there’s a catch. The type-checking and emission rules have gotten a makeover, so don’t be surprised if your favorite decorators from yesteryear don’t play nicely with the new kids on the block.

No worries, though; the future looks bright, with future ECMAScript proposals promising to bring the decorating party to a new level!

Let’s write our first decorator

Let’s start with a simple OnePieceCharacter class.

What is One Piece? Well, it’s the greatest anime ever!

class OnePieceCharacter {
  constructor(private name: string) {}

  greet() {
    console.log(`${this.name} is saying hello`);
  }
}

new OnePieceCharacter('Luffy').greet();
// Luffy is saying hello

This code is straightforward. But the greet function could also contain some complex computations with potential bugs.

Yeah, you can debug using the debugger, but be honest, we all love the good old console.log. Imagine we want to log a statement when we enter the greet function and once we are at the end of the greet function. Easy. We just add the log statements?

greet(){
  console.log('LOG: Entering the method');
  console.log(`${this.name} is saying hello`);
  console.log('LOG: Leaving the method');
}

Simple but still annoying to type. Especially when we want to use this on multiple functions. How about moving this to a decorator?

Let’s just start by creating a simple function that takes two arguments, originalMethod and a context. For the simplicity of this first example, we will just use the any type.

function logMethod(originalMethod: any, context: any) {}

Inside this function, we can return another function, our replacement function.

function logMethod(originalMethod: any, context: any) {
  function replaceMethod(this: any, ...args: any[]) {
    console.log('Entering the method');
    const result = originalMethod.call(this, ...args);
    console.log('Leaving the method');
    return result;
  }

  return replaceMethod;
}

Inside the replaceMethod we put our log statements and then call the originalMethod. It’s important that we use .call and pass the correct context and args to it.

That’s it; at this point, we can apply our decorator to our code.

class OnePieceCharacter {
  constructor(private name: string) {}

  @logMethod
  greet() {
    console.log(`${this.name} is saying hello`);
  }
}

new OnePieceCharacter('Luffy').greet();
// LOG: Entering the method
// Luffy is saying hello
// LOG: Leaving the method

Nice. A reusable decorator. Let’s take this one step further and create a decorator factory.

Everything described in this post was developed live on my Twitch stream. If you are interested in modern web development or just want to chat, you should definitely subscribe to my Channel to not miss future broadcasts.

Decorator factory

Let’s say we want to log our statements with a different prefix. For example DEBUG instead of LOG.

Well, we can quickly achieve that with a decorator factory. To create a factory, we will wrap our function with another function that accepts the logger prefix.

function logWithPrefix(prefix: string) {
  return function actulDecorator(method: any, context: any) {
    function replaceMethod(this: any, ...args: any[]) {
      console.log(`${prefix}: method start`);
      const result = method.call(this, args);
      console.log(`${prefix}: method end`);
      return result;
    }
    return replaceMethod;
  };
}

We can now use this function as decorators and pass in a prefix.

class OnePieceCharacter {
  constructor(private name: string) {}

  @logMethod('DEBUGGER')
  greet() {
    console.log(`${this.name} is saying hello`);
  }
}

new OnePieceCharacter('Luffy').greet();
// DEBUGGER: Entering the method
// Luffy is saying hello
// DEBUGGER: Leaving the method

ClassMethodDecoratorContext

You can imagine that you can go pretty wild with decorators and do a lot of things. You can put custom logic into those decorators to adjust the this, or the args. You can even access the context of the decorated function.

So far, we just typed the parameters of our decorated functions with any. We did this for simplicity, but there are types available that tell us what kind of context we can access.

Let’s refactor our function signature.

function logMethod(originalMethod: any, context: ClassMethodDecoratorContext) {}

We type the context as ClassMethodDecoratorContext. The ClassMethodDecoratorContext has the following signature.

interface ClassMethodDecoratorContext<
  This = unknown,
  Value extends (this: This, ...args: any) => any = (
    this: This,
    ...args: any
  ) => any,
> {
  /** The kind of class member that was decorated. */
  readonly kind: 'method';
  /** The name of the decorated class member. */
  readonly name: string | symbol;
  /** A value indicating whether the class member is a static (`true`) or instance (`false`) member. */
  readonly static: boolean;
  /** A value indicating whether the class member has a private name. */
  readonly private: boolean;
  addInitializer(initializer: (this: This) => void): void;
}

Most properties on this interface are self-explanatory. There is one, though, that probably doesn’t make too much sense at first sight.

addInitializer and bound

The addInitializer function allows us to provide a callback that hooks into class initialization. Nice, but when is this useful?

This becomes useful once you start to pass around functions. Means as soon as the this doesn’t refer to your class. Let’s look at the following example.

class OnePieceCharacter {
  constructor(private name: string) {}

  greet() {
    console.log(`${this.name} is saying hello`);
  }
}

const luffy = new OnePieceCharacter('Luffy');
luffy.greet();
// Luffy is saying hello

const myFunc = luffy.greet;
myFunc();
// undefined is saying hello

This makes perfect sense, right? In the second approach the this refers to the global and not to OnePieceCharacter.

Let’s write a small decorator that fixes this issue.

function bound(originalMethod: any, context: ClassMethodDecoratorContext) {
  const methodName = context.name;
  if (context.private) {
    throw new Error('bound can not be used on private methods');
  }
  context.addInitializer(function () {
    this[methodName] = this[methodName].bind(this);
  });
}

The initializer is added to the class by calling context.addInitializer(function (){ ... }), where the anonymous function inside the addInitializer call is the initializer that binds the method to the instance.

If we now rerun our example, we get the following output.

class OnePieceCharacter {
  constructor(private name: string) {}

  greet() {
    console.log(`${this.name} is saying hello`);
  }
}

const luffy = new OnePieceCharacter('Luffy');
luffy.greet();
// Luffy is saying hello

const myFunc = luffy.greet;
myFunc();
// Luffy is saying hello

Awesome. We learned about the ClassMethodDecoratorContext but note that we still type the first parameter with any. This is okay since we don’t do much with the original method's first param besides calling it.

But we could still come up with a fully typed decorator.

Fully typed decorators

Here’s an example of how a fully typed decorator would look like.

function logMethod<This, Args extends any[], Return>(
  originalMethod: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<
    This,
    (this: This, ...args: Args) => Return
  >,
) {
  function replaceMethod(this: This, ...args: Args) {
    console.log('Start the method');
    const result = originalMethod.call(this, ...args);
    console.log('End the method');
    return result;
  }
  return replaceMethod;
}

🤪 pretty crazy right?

The complexity of the decorator function definition will vary based on the desired level of type safety. A well-typed version of the decorator can significantly improve its usability, but it’s important to strike a balance between type safety and readability.

Do you enjoy the theme of the code preview? Explore our brand new theme plugin

Skol - the ultimate IDE theme

Skol - the ultimate IDE theme

Northern lights feeling straight to your IDE. A simple but powerful dark theme that looks great and relaxes your eyes.

Do you enjoy the content and would like to learn more about how to ensure long term maintainability of your Angular application?

Angular Enterprise Architecture Ebook

Angular Enterprise Architecture Ebook

Learn how to architect a new or existing enterprise grade Angular application with a bulletproof tooling based automated architecture validation.

This will ensure that Your project stays maintainable, extendable and therefore with high delivery velocity over the whole project lifetime!

Get notified
about new blog posts

Sign up for Angular Experts Content Updates & News and you'll get notified whenever I release a new article about Angular, Ngrx, RxJs or other interesting Frontend topics!

We will never share your email with anyone else and you can unsubscribe at any time!

Emails may include additional promotional content, for more details see our Privacy policy.
Kevin Kreuzer - GDE for Angular & Web Technologies

Kevin Kreuzer

GDE for Angular & Web Technologies

Trainer, Berater und Senior Front-End Engineer mit Schwerpunkt auf dem modernen Web. Er ist sehr erfahren in der Implementierung, Wartung und Verbesserung von Anwendungen und Kernbibliotheken für große Unternehmen.

Kevin ist ständig dabei, sein Wissen zu erweitern und zu teilen. Er unterhält mehrere Open-Source-Projekte, unterrichtet moderne Webtechnologie in Workshops, Podcasts, Videos und Artikeln. Weiter ist er ein beliebter Referent auf Konferenzen. Er schreibt für verschiedene Tech-Publikationen und war 2019 der aktivste Autor der beliebten Angular In-Depth Publikation.

58

Blog posts

2M

Blog views

23

NPM packages

3M+

Downloaded packages

39

Videos

14

Celebrated Champions League titles

Responses & comments

Do not hesitate to ask questions and share your own experience and perspective with the topic

You might also like

Check out following blog posts from Angular Experts to learn even more about related topics like TypeScript !

Angular & tRPC

Angular & tRPC

Maximum type safety across the entire stack. How to setup a fullstack app with Angular and tRPC.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@kreuzercode

24.01.2023

6 min read

Advanced TypeScript

Advanced TypeScript

Get familiar with some of Typescript's greatest advanced features.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@kreuzercode

10.11.2022

7 min read

Angular Signal Inputs

Angular Signal Inputs

Revolutionize Your Angular Components with the brand new Reactive Signal Inputs.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@kreuzercode

24.01.2024

6 min read

Stärken Sie Ihr Team mit unserer umfassenden Erfahrung

Unsere Angular Experten haben viele Jahre damit verbracht, Unternehmen und Startups zu beraten, Workshops und Tutorials zu leiten und umfangreiche Open-Source-Ressourcen zu pflegen. Wir sind sehr stolz auf unsere Erfahrung im Bereich des modernen Frontends und würden uns freuen auch Ihrem Unternehmen zum Aufschwung zu verhelfen.

or