With the release of TypeScript five, the language now supports the upcoming ECMAScript decorators standard. In this article, we explore what this means.
Kevin Kreuzer
@nivekcode
Apr 3, 2023
6 min read
The latest version of TypeScript, five, is now out. With this update, support for the forthcoming ECMAScript decorators spec has landed. Let’s see what it entails.
Hold on—upcoming spec? Decorators in TypeScript have been around for ages, so how did they function without this standard?
--experimentalDecorators
Prior to now, TypeScript’s decorator support was labelled experimental, and activating it demanded setting the --experimentalDecorators compiler option.
With this new release, that flag is optional, so decorators work without enabling it.
However, there is a twist. Both the type-checking process and the code generation logic were overhauled, so your familiar, old-school decorators might not integrate smoothly with the current approach.
But the outlook is positive, since newer ECMAScript proposals are set to elevate the decorator experience even further!
Let's write our first decorator
We'll kick things off by defining a simple OnePieceCharacter class.
Not familiar with One Piece? It’s the best anime of all time!
class OnePieceCharacter {
constructor(private name: string) {}
greet() {
console.log(`${this.name} is saying hello`);
}
}
new OnePieceCharacter('Luffy').greet();
// Luffy is saying hello
The logic here is simple. However, the greet function might also perform more intricate operations that could introduce errors.
Sure, you can rely on the debugger, but let's admit it—we all appreciate a classic console.log. Suppose the goal is to log a message on entering the greet function and again once it concludes. Simple enough, right? Just insert the logging calls?
greet(){
console.log('LOG: Entering the method');
console.log(`${this.name} is saying hello`);
console.log('LOG: Leaving the method');
}
It works, but typing it out is tedious—particularly if you need it across several functions. What if we wrap it in a decorator?
To begin, we’ll define a function accepting two parameters: originalMethod and context. For this minimal demonstration, we’ll type both loosely using any.
function logMethod(originalMethod: any, context: any) {}
Within this function, we have the option to return an additional function—this will serve as our substitute.
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;
}
Within replaceMethod, we place our logging logic and subsequently invoke originalMethod. Passing the proper context and args to .call is crucial here.
With that done, we’re ready to use the decorator on our implementation.
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
Great work — that decorator is already reusable. Let’s push it further by building a decorator factory.
All the content in this article was coded live on my Twitch stream. If modern web development interests you, or you simply want to hang out, make sure to hit subscribe on my Channel so you never miss a future stream.
Decorator factory
What if we want our logged messages to carry a custom prefix, like DEBUG instead of LOG?
A decorator factory lets us do that in no time. To build one, we simply wrap our existing function in another function that takes in 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;
};
}
This function can now serve as a decorator, with a prefix passed into it.
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
It should be clear that decorators unlock a wide range of possibilities. Within them, you can insert bespoke logic to alter this or args, and even reach into the context tied to the target function.
Up to now, we’ve kept things simple by typing the parameters of our decorated functions as any. However, dedicated types exist that reveal exactly which context is available.
Now, we’ll update the function signature.
function logMethod(originalMethod: any, context: ClassMethodDecoratorContext) {}
For the context, we use the type ClassMethodDecoratorContext. Here is what the ClassMethodDecoratorContext type signature looks like.
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;
}
The majority of the properties defined on this interface are clear enough on their own, yet there's one that tends to confuse readers when they first encounter it.
addInitializer and bound
With the addInitializer function, we can supply a callback that plugs directly into the process of class instantiation. That's nice, but what's the real-world application here?
The value becomes apparent once you start sharing functions across different contexts, meaning when this no longer points back to your class instance. Consider the scenario below.
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 behavior is entirely logical, isn’t it? In the second version, this points to the global object rather than to OnePieceCharacter.
Now, we can create a compact decorator to resolve this problem.
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 gets attached to the class through context.addInitializer(function (){ ... }), with the anonymous function passed to addInitializer serving as the initializer responsible for linking the method to the instance.
Rerunning the example now produces the output shown below.
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
Great. We've covered the ClassMethodDecoratorContext, but keep in mind the first parameter is still typed as any. That works fine here, since we only invoke the original method rather than inspecting its first argument.
Still, we could build a decorator with full typing.
Fully typed decorators
Consider this illustration of what a completely typed decorator might 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;
}
🤪 completely bonkers, right?
The level of type safety desired dictates how intricate the decorator function’s definition will be. While a precisely typed decorator can boost how intuitive it is to work with, finding the right equilibrium between type safety and clarity is crucial.
Liking the look of the code preview? Check out our fresh theme plugin
Skol - the ultimate IDE theme
Aurora-grade vibes delivered right into your editor. Understated yet striking, this dark theme keeps things easy on the eyes.
Craft sharper interfaces with Angular and AI
Angular + AI Video Course
A practical workshop on embedding AI in Angular applications with Hash Brown, enabling you to craft responsive smart interfaces.
Explore streaming conversations, function invocation, generative component rendering, and structured data extraction at your own pace.
If you find this material valuable and want to deepen your knowledge of keeping your Angular codebase sustainable over time, this resource is for you.
Official Guide to Angular Enterprise Architecture
Discover how to set up rock-solid, automated architecture validation for any Angular application, whether it's brand new or already in production.
As a result, your codebase stays clean, easy to extend and consistently delivers at peak development speed throughout its entire lifecycle!
Like what you're reading and ready to dive deep into Angular's new Signal Forms?
Signal Forms in Angular: A Comprehensive Practical Guide
Explore Angular's innovative Signal-Forms across 12 step-by-step lessons, blending practical exposure with conceptual grounding.
Dive into form fundamentals, validation logic, bespoke controls, nested form structures, and effective upgrade approaches.
Get notified
about new blog posts
Subscribe to Angular Experts Content Updates & News and we will let you know the moment a fresh article on Angular, Ngrx, RxJs, or other fascinating Frontend subjects goes live!
Your email address stays confidential and cancelling your subscription is always possible!
Responses & comments
Feel free to ask about anything and contribute your own viewpoint and insights on the subject
You might also like
Browse these additional posts from Angular Experts to gain deeper knowledge on relevant subjects including TypeScript !

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

Kevin Kreuzer
@nivekcode
Jan 24, 2023
6 min read

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

Kevin Kreuzer
@nivekcode
Nov 10, 2022
7 min read

Angular Signal Forms: Custom Controls Without ControlValueAccessor
Build reusable Angular custom controls with FormValueControl, model(), touch events, and schema-driven validation—without writing a ControlValueAccessor.

Kevin Kreuzer
@nivekcode
Aug 12, 2026
7 min read
Leverage our deep know-how for your team's success
For years, Angular Experts have collaborated with both startups and large enterprises, delivered hands-on trainings and sessions, and contributed to a vibrant open source ecosystem. Our mastery of contemporary front-end development is a source of great pride, and we're eager to help your business excel
