Extending or simplifying TypeScript decorators

Giacomo Voß
Level Up Coding
Published in
3 min readFeb 14, 2020

--

Did you ever use a TypeScript decorator from a library and thought

if this thing could just do like one more thing, it would be perfect!

or maybe

3 of these 4 parameters are always the same, what a waste of time to write the same stuff over and over again!

to yourself? If not, you’re lying or just very modest 😉. If yes, read on.

Decorators are functions

The main thing to know about TypeScript decorators: They look quite fancy, but in their core, they are nothing more than a function that will be executed while the decorated method, property or class is loaded. When you define a decorator, you can either define a function directly or implement a factory that returns a parameterized function.

The final function, either defined or returned by a factory, gets its target as a parameter as a bare minimum. Depending on the type of decorator (i.e. what you put it in front of), the target could be:

  • a method of a class
  • a property of a class
  • a class itself

Read more about the different types of decorators in the official TypeScript documentation.

Example decorator

Let’s look at the following usage of a class decorator:

import {Table} from "imaginary-decorator-library";@Table({
name: "example",
createAutoAttributes: true,
})
export class MyEntity {}

This is a decorator based on a factory, because you can put in parameters to specify details.

Imagine you use that decorator in countless entity files, and you always have to provide an object as a parameter, giving the table’s name and the createAutoAttribute flag, which is always true. But on the other hand, it would be wonderful if the name given here would be put in a global array of names, so that we can check the array on duplications before allowing the decorator.

So let’s implement our own decorator factory:

import {Table} from "imaginary-decorator-library";
import {TABLE_NAME} from "somewhere-else";
export function CoolerTable(name: string): Function {
// Factory returns the actual decoration function.
return function(target: Function): void {

// Check our duplication stuff.
if(TABLE_NAMES.includes(name)) {
throw new Exception(`Duplicate table name "${name}"!`);
}
TABLE_NAMES.push(name);
// Execute the uncool decorator.
Table({
name: name,
createAutoAttributes: true,
})(target);
}
}

The factory gets a name as a parameter and returns a new class decorator, which is an empty function by now. Now let’s insert our own implementation details:

import {Table} from "imaginary-decorator-library";
import {TABLE_NAME} from "somewhere-else";
export function CoolerTable(name: string): Function {
// Factory returns the actual decoration function.
return function(target: Function): void {

// Check our duplication stuff.
if(TABLE_NAMES.includes(name)) {
throw new Exception(`Duplicate table name "${name}"!`);
}
TABLE_NAMES.push(name);
}
}

Right now, you could already use the decorator right away in your code. It would check if the given table name is already used in another decorator, and otherwise throw an exception. But you would still have to apply the original @Table decorator for the actual functionality. So let’s finish this:

import {Table} from "imaginary-decorator-library";
import {TABLE_NAME} from "somewhere-else";
export function CoolerTable(name: string): Function {
// Factory returns the actual decoration function.
return function(target: Function): void {

// Check our duplication stuff.
if(TABLE_NAMES.includes(name)) {
throw new Exception(`Duplicate table name "${name}"!`);
}
TABLE_NAMES.push(name);
// Execute the uncool decorator.
Table({
name: name,
createAutoAttributes: true,
})(target);
}
}

See what I did there? Because decorators are, as we learned, functions – you can invoke them yourself on a target. In the last lines of our custom decorator, we first invoke the factory to get a decorator function, and then invoke the function itself, with the original target as a parameter. This way, you can do everything you want before and after applying the decorator, and customize its inputs.

Bonus tip

Don’t you hate it when you always have to apply two or more decorators do get your desired behaviour? Just create a new decorator and invoke both of them in its implementation!

--

--