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

Apr 3, 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 think that your teammates or organization could benefit from more direct support?

Angular Enterprise Architecture Ebook

Angular Enterprise Architecture Ebook

Learn how to architect and scaffold a new enterprise grade Angular application with clean, maintainable and extendable architecture in almost no time!

Lots of actionable tips and pros & cons of specific decisions based on the extensive experience!

Get notified
about new blog posts

Sign up for Angular Experts Content Updates & News and you'll get notified whenever we release a new blog posts 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

Google Developer Expert (GDE)
for Angular & Web Technologies

A trainer, consultant, and senior front-end engineer with a focus on the modern web, as well as a Google Developer Expert for Angular & Web technologies. He is deeply experienced in implementing, maintaining and improving applications and core libraries on behalf of big enterprises worldwide.

Kevin is forever expanding and sharing his knowledge. He maintains and contributes to multiple open-source projects and teaches modern web technologies on stage, in workshops, podcasts, videos and articles. He is a writer for various tech publications and was 2019’s most active Angular In-Depth publication writer.

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

Jan 24, 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

Nov 10, 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

Jan 24, 2024

6 min read

Empower your team with our extensive experience

Angular Experts have spent many years consulting with enterprises and startups alike, leading workshops and tutorials, and maintaining rich open source resources. We take great pride in our experience in modern front-end and would be thrilled to help your business boom

or