Dependency Injection, a deep dive into Typescript Decorators
Lukas Gamper, uSystems GmbH
Webling

Outline
- Introduction to decorators
- Decorator metadata
- Dependency injection
Decorators
- Annotate / modify classes and class members
-
Syntax: @expression
-
where expression evaluates to a function
-
- Can be attached to class declarations, methods, accessors, properties or parameters
- Evaluated at runtime
-
Stage 2 proposal for JavaScript
class C {
@decorator
method() {}
}
Configuration
Decorators are an experimental feature that may change in future releases.
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
tsc --target ES5 --experimentalDecorators
Command Line:
tsconfig.json:
Class Decorators
- Declared before the class
- Applied to the constructor of the class
-
- replace declaration with return value, if any
@decorator(constructor)
Class Decorators
function sealed(ctor: Function) {
Object.seal(ctor);
Object.seal(ctor.prototype);
}
@sealed
class Greeter {
private whom: string;
constructor(whom: string) {
this.whom = greeting;
}
public greet() {
return "Hello, " + this.whom;
}
}
var __decorate = ...;
function sealed(ctor) {
Object.seal(ctor);
Object.seal(ctor.prototype);
}
var Greeter = (function () {
function Greeter(whom) {
this.whom = whom;
}
Greeter.prototype.greet
= function () {
return "Hello, " + this.whom;
};
Greeter = __decorate([
sealed
], Greeter);
return Greeter;
}());
function sealed(ctor: Function) {
Object.seal(ctor);
Object.seal(ctor.prototype);
}
@sealed
class Greeter {
private whom: string;
constructor(whom: string) {
this.whom = greeting;
}
public greet() {
return "Hello, " + this.whom;
}
}
Property Descriptor
class Immutable {
private _value: number;
constructor(value: number) {
this._value = value;
}
get value() {
return this._value;
}
set value(value: number) {
this._value = value;
}
}
var Immutable = (function () {
function Immutable(value) {
this._value = value;
}
Object.defineProperty(
Immutable.prototype,
"value",
{
get: function () {
return this._value;
},
set: function (value) {
this._value = value;
},
enumerable: true,
configurable: true
}
);
return Immutable;
}());
Method Decorators
- Declared before the method
- Applied to the property descriptor of the method
-
- replace property descriptor with return value, if any
@decorator(ctorOrProto, methodName, PropertyDescriptor)
Method Decorators
function enumerable(value: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.enumerable = value;
};
}
class Greeter {
private greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
public greet() {
return "Hello, " + this.greeting;
}
}
Accessor Decorators
- Declared before the accessor declaration (get, set)
- Applied to property descriptor of accessor declaration
- Only one decorator before the first accessor specified
-
- replace property descriptor with return value, if any
@decorator(ctorOrProto, accessorName, PropertyDescriptor)
Accessor Decorators
function configurable(value: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.configurable = value;
};
}
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() { return this._x; }
@configurable(true)
get y() { return this._y; }
}
Property Decorators
- Declared before the property declaration
-
- no property descriptor provided
- return value ignored
@decorator(ctorOrProto, propertyName)
Parameter Decorators
- Declared before the parameter declaration
-
- return value ignored
@decorator(ctorOrProto, parameterName, indexInTheParameterList)
Decorator Factories
- Function returning the decorator expression at runtime
function enumerable(value: boolean) {
return function (target: any, key: string, desc: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
class Greeter {
private greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
public greet() {
return "Hello, " + this.greeting;
}
}
Decorator Metadata
- Design-time type information added, using the @Reflect.metadata
- Available metadata:
- Type metadata as "design:type"
- Parameter type metadata as "design:paramtypes"
- Return type metadata as "design:returntype".
- Not yet part of the ECMAScript standard
Configuration
Decorator metadata is an experimental feature that may change in future releases.
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
Command Line:
tsconfig.json:
Decorator Metadata
import "reflect-metadata";
class Point {
public x: number;
public y: number;
}
class Line {
private _p0: Point;
private _p1: Point;
@validate
set p0(value: Point) { this._p0 = value; }
get p0() { return this._p0; }
@validate
set p1(value: Point) { this._p1 = value; }
get p1() { return this._p1; }
}
function validate<T>(target: any, key: string, desc: TypedPropertyDescriptor<T>) {
let originalSet = desc.set;
desc.set = function(value: T) {
let type = Reflect.getMetadata("design:type", target, key);
if (!(value instanceof type)) {
throw new TypeError("Invalid type.");
}
originalSet(value);
}
}
var __decorate = ...;
var __metadata = ...;
var Point = ...;
var Line = (function () {
function Line() {}
Object.defineProperty(Line.prototype, "p0", {
get: function () { return this._p0; },
set: function (value) { this._p0 = value; },
enumerable: true,
configurable: true
});
Object.defineProperty(Line.prototype, "p1", ...);
__decorate([
validate,
__metadata("design:type", Point),
__metadata("design:paramtypes", [Point])
], Line.prototype, "p0", null);
__decorate([...], Line.prototype, "p1", null);
return Line;
}());
function validate(target, propertyKey, descriptor) ...
Decorator Metadata
import "reflect-metadata";
class Line {
private _p0: Point;
private _p1: Point;
@validate
@Reflect.metadata("design:type", Point)
@Reflect.metadata("design:paramtypes", [Point])
set p0(value: Point) { this._p0 = value; }
get p0() { return this._p0; }
@validate
@Reflect.metadata("design:type", Point)
@Reflect.metadata("design:paramtypes", [Point])
set p1(value: Point) { this._p1 = value; }
get p1() { return this._p1; }
}
Dependency Injection
- Injector supplies the dependencies of another objects
- Improve testability, maintainability, readability
- Without dependency injection
- With dependency injection
new LoginModel(new UserService(new Transport()))
Injector.resolve<LoginModel>(LoginModel)
Injector
interface Constructor<T> {
new(...args: any[]): T;
}
const Injector = new class {
public resolve<T>(target: Constructor<any>): T {
const tokens =
Reflect.getMetadata('design:paramtypes', target) || [];
const injections =
tokens.map(token => Injector.resolve<any>(token));
return new target(...injections);
}
};
const injectable = (target: Constructor<object>) => {};
Example
@injectable
class Foo {
constructor() {
console.log('foo');
}
}
@injectable
class Bar {
constructor(public foo: Foo) {
console.log('bar');
}
}
@injectable
class Foobar {
constructor(public bar: Bar, public foo: Foo) {
console.log('foobar');
}
}
const foobar = Injector.resolve<Foobar>(Foobar);
foo
foo
bar
foo
bar
foo
foo
bar
foo
foobar
foo
bar
foo
bar
foo
foobar
Disclaimer
- Error handling
- Singleton dependencies
- Protection of circular dependencies
- Inject non constructor tokens
Caveats
- Interfaces are gone after transpilation
→ use classes instead of interfaces - Circular dependencies causes trouble
→ e.g. Angulars forwardRef - Classes without decorators have no metadata
Thanks
Dependency Injection, a deep dive into Typescript Decorators
By gamperl
Dependency Injection, a deep dive into Typescript Decorators
- 365