TypeScript Decorators
Let's learn how to decorate our cak... code with TypeScript Decorators. Decorators are a tasty addition to classes and provide meta-programming syntax.
Last updated: October 21, 2023Decorators are an experimental feature in TypeScript. They were added in TypeScript 5.0 and can be attached to 5 different things: class declaration, method, accessor, property, and parameter. So basically, they are all about the bass...class, I mean. You can use them for meta-programming - a technique in which a program has knowledge of itself or can manipulate itself. They don't have a direct impact on the end user. It's a well-suited instrument for writing code that is easier to use by other developers. Let's tune our brain instruments to learn some decorators!
At the time of writing, decorators are an upcoming ECMAScript feature. They are at stage 3 and may be added to the native JavaScript. There is a paragraph describing ECMAScript specification in one of my previous posts.
Setup
Before learning decorators, we need a little work with the setup. To enable experimental support for decorators, you have two options.
You can add the option for the TypeScript CLI.
🔴 🟡 🟢
tsc --target ES6 --experimentalDecorators
Or you can add it to the tsconfig.json
.
JSON
1{2 "compilerOptions": {3 "target": "es6",4 "experimentalDecorators": true5 }6}
Defining Decorators
Having formalities out of the way, we can dive into decorators. A decorator, in the end, is just a standard JavaScript function. A function you apply to something. To apply a decorator, you need the at symbol (@). TypeScript uses this symbol to recognize a decorator.
TYPESCRIPT
1function Logger(target: Function) {2 console.log('Logging...')3 console.log(target)4}56@Logger7class Agent {8 name = 'Sam'910 constructor() {11 console.log('Creating a Third Echelon agent')12 }13}
Decorators, similarly to functions, can receive arguments. The arguments depend on the type of decorator. The class decorator gets one, target
- it's the constructor of the class. If the class decorator returns a value, it will replace the class declaration. We'll go through more types of decorators later.
Decorators usually use PascalCase (or UpperCamelCase) - you should capitalize the first letters.
Decorator Factory
We can nest another function in our decorator, and this way, we're getting the decorator factory. The decorator factory returns a decorator function. We can configure a decorator before attaching it to something using this pattern. If you're getting a higher-order function vibe from this, your intuition is on point.
TYPESCRIPT
1function Logger(message: string) {2 return function (constructor: Function) {3 console.log(message)4 console.log(constructor)5 }6}78@Logger('Logging in agent...')9class Agent {10 name = 'Sam'1112 constructor() {13 console.log('Creating a Third Echelon agent')14 }15}1617@Logger('Logging in civilian...')18class Civilian {19 name = 'Sarah'2021 constructor() {22 console.log('Creating a civilian')23 }24}
We can use parameters to pass values used by that inner decorator. It gives us flexibility in configuring what the decorator does internally. This way, we can generate multiple similar yet different decorators.
Multiple Decorators
“How about attaching more than one decorator to something? Can we do that?” Yes, we can add more than one decorator to a class. Or anywhere decorators apply. The syntax is simple - stack decorators, one on the other, like pancakes in the morning.
TYPESCRIPT
1function Greeting(constructor: Function) {2 console.log('Hello!')3}45function Logger(constructor: Function) {6 console.log('Logging...')7}89@Logger10@Greeting11class Agent {12 name = 'Sam'1314 constructor() {15 console.log('Creating a Third Echelon agent')16 }17}
That raises another question - in which order do they execute? They execute bottom-up. The bottom-most decorator first, then the decorators above it.
TYPESCRIPT
1function FirstDecorator(constructor: Function) {2 console.log('This message will be logged first')3}45function SecondDecorator(constructor: Function) {6 console.log('This message will be logged second')7}89@SecondDecorator10@FirstDecorator11class Agent {12 name = 'Sam'1314 constructor() {15 console.log('Creating a Third Echelon agent')16 }17}
“Okay, and how about factories? Do they also execute bottom-up?” Actually...no. The opposite - they run top to bottom - in standard execution order. It may be misleading, so I wrote a rhyme to help us remember:
Roses are red, violets are blue,
decorators run bottom-up,
and for factories that's not true.
TYPESCRIPT
1function FirstDecoratorFactory() {2 console.log('Message from this factory will be logged first')34 return function (constructor: Function) {5 console.log('Message from this decorator will be logged second')6 }7}89function SecondDecoratorFacotry() {10 console.log('Message from this factory will be logged second')1112 return function (constructor: Function) {13 console.log('Message from this decorator will be logged first')14 }15}1617@FirstDecoratorFactory()18@SecondDecoratorFacotry()19class Agent {20 name = 'Sam'2122 constructor() {23 console.log('Creating a Third Echelong agent')24 }25}
When do decorators execute?
While we're touching on execution, let's grab the subject and check when decorators run. If you look into previous code snippets, you'll see that none of the above classes were instantiated. Nevertheless, you'll see the messages in the console. It's because all decorators (no matter the type) execute when you define a class. Not when you instantiate it. They don't run at runtime. They allow you to do additional, behind-the-scenes setup work when the class is defined.
Types of Decorators
I mentioned different types of decorators. We have options when it comes to attaching decorators. Earlier, we saw class decorators. But we don't have to apply them directly to classes. We can add decorators, for example, to the property. Which arguments a decorator gets depends on where you add it.
A similar story is with return values. Some decorators are capable of returning something. What you can get back depends on which kind of decorator you work with. However, only in some decorators, the return value is respected. Let's go through different decorator types.
Property Decorators
The property decorator receives two arguments:
target
- it can be either:- The constructor function of the class - for a static member.
- The prototype of the class - for a class instance member.
propertyKey
- the name of the property.
The return value will be ignored.
TYPESCRIPT
1function Log(target: any, propertyName: string | symbol) {2 console.log('Property Decorator')3 console.log(target, propertyName)4}56class Agent {7 @Log8 name: string910 constructor() {11 console.log('Creating a Third Echelon agent')12 }13}
Accessor Decorators
To access properties, we can use accessors. And we can add decorators to accessors as well. The arguments they receive are:
target
- it can be either:- The constructor function of the class - for a static member.
- The prototype of the class - for a class instance member.
propertyKey
- the name of the property.descriptor
- the property descriptor for the member. The descriptor in the accessor decorator has options:get
set
enumerable
configurable
The return value will be used as the descriptor of the member if returned.
TYPESCRIPT
1function Log(2 target: any,3 propertyName: string | symbol,4 descriptor: PropertyDescriptor5) {6 console.log('Accessor Decorator')7 console.log(target)8 console.log(name)9 console.log(descriptor)10}1112class Agent {13 private _name: string1415 @Log16 set name(value: string) {17 this._name = value18 }1920 constructor() {21 console.log('Creating a Third Echelon agent')22 }23}
Method Decorators
Method decorators are very similar to the ones for accessors. The main difference is the options in the descriptor:
target
- it can be either:- The constructor function of the class - for a static member.
- The prototype of the class - for a class instance member.
propertyKey
- the name of the property.descriptor
- the property descriptor for the member. We can use this parameter to override the original implementation and inject some logic. The descriptor in the method decorator has options:value
writable
enumerable
configurable
The return value will be used as the descriptor of the member if returned.
TYPESCRIPT
1function Log(2 target: any,3 name: string | symbol,4 descriptor: PropertyDescriptor5) {6 console.log('Method Decorator')7 console.log(target)8 console.log(name)9 console.log(descriptor)10}1112class Agent {13 name = 'Sam'1415 @Log16 getName() {17 return this.name18 }1920 constructor() {21 console.log('Creating a Third Echelon agent')22 }23}
Parameter Decorators
You can even add decorators to the individual parameters in a method. This type of decorator also takes three arguments:
target
- it can be either:- The constructor function of the class - for a static member.
- The prototype of the class - for a class instance member.
propertyKey
- the name of the property. But be aware - it's the name of the method. Not the name of the parameter.parameterIndex
- the parameter position in the function's parameter list.
The return value will be ignored.
TYPESCRIPT
1function Log(target: any, name: string | symbol, parameterIndex: number) {2 console.log('Parameter Decorator')3 console.log(target)4 console.log(name)5 console.log(position)6}78class Agent {9 name = 'Sam'1011 greetings(@Log name: string) {12 console.log(`Hello ${name}!`)13 }1415 constructor() {16 console.log('Creating a Third Echelon agent')17 }18}
To wrap up decorator types, I prepared a summary table.
Decorator | Class | Property | Accessor | Method | Parameter |
---|---|---|---|---|---|
Argument 1 | target | target | target | target | target |
Argument 2 | propertyKey | propertyKey | propertyKey | propertyKey | |
Argument 3 | descriptor | descriptor | parameterIndex | ||
Return value | Class declaration | Ignored | Descriptor | Descriptor | Ignored |
Decorator Examples
Angular
One of the most popular frameworks that uses TypeScript is Angular. Google decided that Angular 2 would be based on TypeScript entirely, citing static typing as one of the reasons. Static types are definitely an advantage, but we'll focus on decorators. Let's see how Angular uses decorators.
TYPESCRIPT
1@Component({2 selector: 'app-agent',3 inputs: ['name'],4 template: ` Agent Name: {{ agentName }} `5})6export class AgentComponent {7 name: string | null = null8}
The above example presents a recognizable Component
decorator. This decorator marks a class as an Angular component. It accepts configuration and metadata that determines how this basic UI block should look and behave at runtime. You can pass there an HTML template, styling, or schemas.
NestJS
NestJS is a Node.js framework for building scalable, server-side applications with TypeScript. It's popular among Back-end developers. NestJS also relies on decorators. If you've used NestJS, you may have seen decorators in controllers.
TYPESCRIPT
1import { Controller, Get } from '@nestjs/common'23@Controller('/agents')4export class AgentController {5 @Get()6 findAll(): string {7 return 'This action returns all agents'8 }9}
Controllers are responsible for handling incoming requests and returning responses to the client. The Controller decorator will register the class in the metadata as a controller for the specified HTTP route. So, decorators in NestJS are used i.a. for the routing mechanism.
Summary
Even though decorators don't influence end users, they improve the developer's experience. You can use them to watch property changes, transform parameters, runtime validation, and many other ways. They provide an elegant syntax to modify or extend the behavior of a class. Newly, TypeScript offers this syntax as a part of the language. Check my previous posts to learn more about OOP or TypeScript in general.