This post serves as introduction to TypeScript decorators. It looks at basic decorators, decorator factories, and decorator composition. You should have some familiarity with TypeScript and some object-oriented programming experience.
The Series so Far
- Decorator Introduction
- JavaScript Foundation
- Reflection
- Parameter Decorators
- Property Decorators
- Method Decorators
- Class Decorators
Eventual Topics:
- Where Decorators Work
- Decorating Instance Elements vs. Static Elements
- Examples
- Pairing Parameter Decorators with Method Decorators
- Pairing Property Decorators with Class Decorators
Code
You can view the code related to this post under the post-01-decorator-intro
tag.
Decorators
The decorator pattern modifies instances of existing objects without affecting the root object or siblings. Typically the pattern extends a base interface by toggling features, setting attributes, or defining roles. Instances of the object being decorated should usually be able to interact, but they don’t have to have identical interfaces. Like many foundational patterns, no one agrees about the Platonic decorator.
TypeScript provides experimental decorator support. The ECMAScript decorator proposal has reached stage 2, so we could see them in vanilla JS eventually. TypeScript provides class, method, parameter, and property decorators. Each can be used to observe the decorated objects (mentioned heavily in the docs). All but the parameter decorator can be used to modify the root object.
TypeScript decorators also provide some mixin support. Without true multiple inheritance in JavaScript, combining features can lead to obscenely long prototype chains. TypeScript decorators alleviate that issue by adding behavior at runtime on top of normal inheritance.
Configuration
To gain decorator functionality, you’ll have to pass a few new options to the TypeScript compiler.
target
: The docs mention some issues belowES5
(ctrl+fES5
). I tend to runESNext
while developing.experimentalDecorators
: This is what enables the functionality.emitDecoratorMetadata
: This is another expermental feature that provides decorator metadata.
You can either include the options by hand every time
$ tsc --target 'ESNext' --experimentalDecorators --emitDecoratorMetadata |
or you can add them to your tsconfig.json
once.
tsconfig.json | |
1 2 3 4 5 6 7 | { |
Simple Example
First we need to define several decorators. Each signature was taken from the official docs and will be explained more later (but maybe not this post).
decorators/ClassDecorator.ts | |
1 2 3 4 5 | export function ClassDecorator( |
decorators/MethodDecorator.ts | |
1 2 3 4 5 6 7 8 9 10 | export function MethodDecorator( |
decorators/ParameterDecorator.ts | |
1 2 3 4 5 6 7 8 9 10 11 | export function ParameterDecorator( |
decorators/PropertyDecorator.ts | |
1 2 3 4 5 6 7 8 9 | export function PropertyDecorator( |
Next we’ll need to consume the decorators. The decorators are placed before the object they modify, e.g. @ClassDecorator class Foo {}
. You could use any of the decorators on any object, but you probably won’t see great results unless you hit something like their intended targets. Do note that method decorators are used to modify both normal methods and (g|s)etter
methods.
main.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import { ClassDecorator } from "./decorators/ClassDecorator"; |
$ ts-node main.ts |
The execution order is explained in the docs; to summarize,
- instance parameter, method, and property decorators;
- static parameter, method, and property decorators;
- constructor parameter decorators; and
- class decorators.
Decorator Factories
Decorators have well-defined signatures without room for extension. To pass new information into the decorators, we can use the factory pattern. A factory provides a uniform creation interface whose details are delegated to and managed by children.
decorators/Decorator.ts | |
1 2 3 4 5 | export function Decorator(type: string) { |
In this example, Decorator
takes a string as input and creates a Function
. Changing the input will create a new Function
, but all of the Function
s log the original input string followed by an array containing the args that the child was called with.
$ ts-node |
We can use this decorator everywhere thanks to rest parameters.
main.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import { Decorator } from "./decorators/Decorator"; |
$ ts-node main.ts |
While this example was fairly simple, decorator factories are capable of much more. Anything you pass to the factory can be used to assemble the decorator. As the decorator’s return is used by everything except for parameter decorators, you can customize the instance using anything in the scope. Decorators aren’t limited to building up; they can also tear down.
decorators/MaskMethod.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | export function MaskMethod(hide: boolean) { |
This decorator can hide methods at run time by tweaking the property descriptor for the method.
other-main.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import { MaskMethod } from "./decorators/MaskMethod"; |
$ ts-node other-main.ts |
Composition
Function composition is a very useful tool. It requires two functions, f: A → B
and g: C → D
, with some conditions on their domains and ranges. To compose f
with g
, i.e. f(g(x))
, D
must be a subset of A
, i.e. the input of f
must contain the output of g
.
This is much simpler in code. For the most part, we can compose f
with g
when g
’s return value is identical to f
’s input (completely ignoring containment because that gets messy). As we’ve seen, decorators seem to return a single object while they consume an array of arguments. That would suggest they cannot be composed. However, decorators aren’t actually being called and run on the stack by themselves. TypeScript surrounds the decorator calls with several other things behind the scenes, which, rather magically, means decorators can be composed with other decorators.
decorators/Enumerable.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | export function Enumerable(enumerable: boolean = true) { |
This decorator updates the enumerable
property of methods, showing/hiding them when iterating over the object. To illustrate how it works, this class has two methods that are only decorated once. To illustrate composition, another two are decorated twice.
main.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import { Enumerable } from "./decorators/Enumerable"; |
$ ts-node main.ts |
The first decorator factory builds its factory first, but executes the factory last. The second decorator’s build and execution are sandwiched between the two components of the first. The more decorators chained, the deeper the nesting. To resolve the composition, each call must be finished in turn.
Recap
Decorators provide a way for children to manage their responsibilities and options. TypeScript supports decorators (experimentally for now) with a very simple interface. When basic decorators don’t cut it, the vanilla options can be extended with decorator factories. Composing decorators with decorators allows us to combine multiple decorators on the same object.
I think I’m going to look at the generated JavaScript next. Don’t hold me to that.
Legal
The TS logo is a modified @typescript
avatar; I turned the PNG into a vector. I couldn’t find the original and I didn’t see any licensing on the other art, so it’s most likely covered by the TypeScript project’s Apache 2.0 license. Any code from the TS project is similarly licensed.
If there’s a problem with anything, my email’s in the footer. Stay awesome.