TypeScript Decorators: Introduction

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.

Code

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.

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.

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.

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 Functions log the original input string followed by an array containing the args that the child was called with.

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.

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.

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.

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.