October 21, 2023 10 min read

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, 2023
Teal icing cupcakes decorated with sprinkles
Photo by Brooke Lark

Decorators 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": true
5 }
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}
5
6@Logger
7class Agent {
8 name = 'Sam'
9
10 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}
7
8@Logger('Logging in agent...')
9class Agent {
10 name = 'Sam'
11
12 constructor() {
13 console.log('Creating a Third Echelon agent')
14 }
15}
16
17@Logger('Logging in civilian...')
18class Civilian {
19 name = 'Sarah'
20
21 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}
4
5function Logger(constructor: Function) {
6 console.log('Logging...')
7}
8
9@Logger
10@Greeting
11class Agent {
12 name = 'Sam'
13
14 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}
4
5function SecondDecorator(constructor: Function) {
6 console.log('This message will be logged second')
7}
8
9@SecondDecorator
10@FirstDecorator
11class Agent {
12 name = 'Sam'
13
14 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')
3
4 return function (constructor: Function) {
5 console.log('Message from this decorator will be logged second')
6 }
7}
8
9function SecondDecoratorFacotry() {
10 console.log('Message from this factory will be logged second')
11
12 return function (constructor: Function) {
13 console.log('Message from this decorator will be logged first')
14 }
15}
16
17@FirstDecoratorFactory()
18@SecondDecoratorFacotry()
19class Agent {
20 name = 'Sam'
21
22 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}
5
6class Agent {
7 @Log
8 name: string
9
10 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: PropertyDescriptor
5) {
6 console.log('Accessor Decorator')
7 console.log(target)
8 console.log(name)
9 console.log(descriptor)
10}
11
12class Agent {
13 private _name: string
14
15 @Log
16 set name(value: string) {
17 this._name = value
18 }
19
20 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: PropertyDescriptor
5) {
6 console.log('Method Decorator')
7 console.log(target)
8 console.log(name)
9 console.log(descriptor)
10}
11
12class Agent {
13 name = 'Sam'
14
15 @Log
16 getName() {
17 return this.name
18 }
19
20 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}
7
8class Agent {
9 name = 'Sam'
10
11 greetings(@Log name: string) {
12 console.log(`Hello ${name}!`)
13 }
14
15 constructor() {
16 console.log('Creating a Third Echelon agent')
17 }
18}

To wrap up decorator types, I prepared a summary table.

DecoratorClassPropertyAccessorMethodParameter
Argument 1targettargettargettargettarget
Argument 2propertyKeypropertyKeypropertyKeypropertyKey
Argument 3descriptordescriptorparameterIndex
Return valueClass declarationIgnoredDescriptorDescriptorIgnored

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 = null
8}

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'
2
3@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.

A newsletter that sparks curiosity💡

Subscribe to my newsletter and get a monthly dose of:

  • Front-end, web development, and design news, examples, inspiration
  • Science theories and skepticism
  • My favorite resources, ideas, tools, and other interesting links
I am not a Nigerian prince to offer you opportunities. I do not send spam. Unsubscribe anytime.

Stay curious. Read more

Half of a record on white backgroundSeptember 1, 20227 min read

Accessible animations in React

Or how not to spin your users round (like a record). Some animations can make users sick. We'll take care of them and make non-essential animations optional.

Read post
List of CSS variables in Visual Studio Code.September 14, 20228 min read

Converting design tokens to CSS variables with Node.js

Converting design tokens is an error-prone process - I found about it the hard way. So, I made a simple Node.js script that will help me with that task.

Read post
Five metal gears on a black brackgroundSeptember 23, 202211 min read

Gatsby with Netlify CMS

In this post, we will look closely at a Netlify CMS. It is an example of a new type of CMS that is git-based. We will integrate it with a Gatsby example project.

Read post