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.
Kevin Kreuzer
@kreuzercode
Apr 3, 2023
6 min read
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
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
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 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!
Responses & comments
Do not hesitate to ask questions and share your own experience and perspective with the topic
Nivek
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
39
NPM packages
4M+
Downloaded packages
100+
Videos
15
Celebrated Champions League titles
You might also like
Check out following blog posts from Angular Experts to learn even more about related topics like TypeScript !
Angular & tRPC
Maximum type safety across the entire stack. How to setup a fullstack app with Angular and tRPC.
Kevin Kreuzer
@kreuzercode
Jan 24, 2023
6 min read
Advanced TypeScript
Get familiar with some of Typescript's greatest advanced features.
Kevin Kreuzer
@kreuzercode
Nov 10, 2022
7 min read
Hawkeye, the Ultimate esbuild Analyzer
Effortlessly analyze your JS bundles and uncover actionable insights to boost performance and enhance user experience.
Kevin Kreuzer
@kreuzercode
Dec 28, 2024
7 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