Unlock the Power of TypeScript Decorators: From Basics to IoC and DI
This article walks you through the origins, types, and practical uses of TypeScript decorators, explains metadata reflection, demonstrates decorator factories, and shows how they enable inversion of control and dependency injection in modern backend frameworks like NestJS and Midway.
Preface
This article is for readers who have little experience with TypeScript decorators or have only dabbled with them. It starts with the background of decorators, introduces the various kinds and their use‑cases, then covers metadata reflection and IoC mechanisms, so you will understand exactly how they work.
Introduction to TypeScript Decorators
Simply put, a decorator is syntax applied to a class or its members; under the hood it is just a function. Decorators can hide implementation details, e.g.:
@InternalChanges()
class Foo {}With @InternalChanges() you can completely modify the class behavior with a single line. Although this may seem like a black box, a reusable decorator is essentially a utility function that external users don’t need to understand.
Another widely used feature of decorators is metadata (metaprogramming), which we will explore later.
It is important to note that JavaScript and TypeScript decorators are not the same . JavaScript decorators are still at stage 2, while TypeScript implements the earlier proposal (stage 1). The two implementations differ significantly.
If you have used TypeScript decorators before, the diagram below (showing current JavaScript decorator usage) highlights the gap between the two specifications.
Strictly speaking, decorators are not a TypeScript‑specific feature; they are part of an ECMAScript proposal, similar to private class fields.
TypeScript supports proposals that have reached at least stage‑3, such as optional chaining and nullish coalescing (introduced in TS 3.7). When TypeScript added decorator support (around version 1.5 in 2015), the ECMAScript decorator proposal was still at stage‑1. The collaboration between the TypeScript and Angular teams led to the introduction of annotation syntax.
AtScript was originally built on top of TypeScript and borrowed some features from Dart. Angular 2.0 was also based on AtScript. When TypeScript 1.5 was released, many AtScript features were merged into TypeScript, and Angular 2.0 started using TypeScript directly. The name “AtScript” comes from the heavy use of the @ symbol in Angular.
Even if decorators never reach stage‑3/4, they will not disappear. Many frameworks rely heavily on decorators, such as Angular, Nest, and Midway. Their internal implementation and compiled output will remain, although future JavaScript decorator changes might affect them.
Why do we need decorators? In later examples you will see how they enable elegant logic reuse and capability enhancement, especially for dependency injection using metadata reflection.
Decorators vs. Annotations
I have never learned Java or Spring IoC, so my understanding may be imperfect; please point out any mistakes.
Annotations (in Java) merely provide metadata and cannot modify behavior without an external scanner. Pure decorators, on the other hand, can act on the class or its members directly. In TypeScript, decorators usually combine both capabilities: they consume metadata and can also provide metadata for other decorators.
Different Types of Decorators and Their Usage
To run the examples locally, ensure experimentalDecorators and emitDecoratorMetadata are enabled in tsconfig.json .
Class Decorator
function addProp(constructor: Function) {
constructor.prototype.job = 'fe';
}
@addProp
class P {
job: string;
constructor(public name: string) {}
}
let p = new P('林不渡');
console.log(p.job); // feWhen you use @addProp on any class, the same property is added because the decorator logic is fixed. To make the value configurable, you can turn the decorator into a factory:
function addProp(param: string): ClassDecorator {
return (constructor: Function) => {
constructor.prototype.job = param;
};
}
@addProp('fe+be')
class P {
job: string;
constructor(public name: string) {}
}
let p = new P('林不渡');
console.log(p.job); // fe+beIn TypeScript, a decorator is syntactic sugar for a function that may return another function when used with parentheses.
Method Decorator
A method decorator receives the prototype (or constructor for static members), the property name, and the descriptor ( writable, enumerable, configurable). You can modify the descriptor to, for example, make a method read‑only:
function addProps(): MethodDecorator {
return (target, propertyKey, descriptor) => {
descriptor.writable = false;
};
}
class A {
@addProps()
originMethod() {
console.log("I'm Original!");
}
}
const a = new A();
a.originMethod = () => console.log("I'm Changed!");
// Still logs "I'm Original!"
a.originMethod();Property Decorator
Property decorators receive the prototype and the property name (no descriptor). They are mainly used to inject or read metadata.
function addProps(): PropertyDecorator {
return (target, propertyKey) => {
console.log(target);
console.log(propertyKey);
};
}
class A {
@addProps()
originProps: unknown;
}Parameter Decorator
Parameter decorators receive the prototype (or constructor for static members), the property name, and the parameter index. They cannot modify the descriptor, but they can store metadata on the prototype for later use.
function paramDeco(params?: any): ParameterDecorator {
return (target, propertyKey, index) => {
target.constructor.prototype.fromParamDeco = 'Foo';
};
}
class B {
someMethod(@paramDeco() param1: unknown, @paramDeco() param2: unknown) {
console.log(`${param1} ${param2}`);
}
}
new B().someMethod('A', 'B');
console.log(B.prototype.fromParamDeco); // FooDecorator Factories
When you need several similar decorators, a factory can generate them based on conditions.
function classDeco(): ClassDecorator {
return target => console.log('Class Decorator Invoked', target);
}
function propDeco(): PropertyDecorator {
return (target, propertyKey) => console.log('Property Decorator Invoked', propertyKey);
}
function methodDeco(): MethodDecorator {
return (target, propertyKey, descriptor) => console.log('Method Decorator Invoked', propertyKey);
}
function paramDeco(): ParameterDecorator {
return (target, propertyKey, index) => console.log('Param Decorator Invoked', propertyKey, index);
}
enum DecoratorType { CLASS = 'CLASS', METHOD = 'METHOD', PROPERTY = 'PROPERTY', PARAM = 'PARAM' }
type FactoryReturnType = ClassDecorator | MethodDecorator | PropertyDecorator | ParameterDecorator;
function decoFactory(this: any, type: DecoratorType, ...args: any[]): FactoryReturnType {
switch (type) {
case DecoratorType.CLASS: return classDeco.apply(this, args);
case DecoratorType.METHOD: return methodDeco.apply(this, args);
case DecoratorType.PROPERTY: return propDeco.apply(this, args);
case DecoratorType.PARAM: return paramDeco.apply(this, args);
default: throw new Error('Invalid DecoratorType');
}
}
@decoFactory(DecoratorType.CLASS)
class C {
@decoFactory(DecoratorType.PROPERTY)
prop: unknown;
@decoFactory(DecoratorType.METHOD)
method(@decoFactory(DecoratorType.PARAM) param: string) {}
}
new C().method('foobar');Execution Order of Multiple Decorators
When multiple decorators are applied, the order is:
Parameter decorators → method/accessor/property decorators for each instance member.
Parameter decorators → method/accessor/property decorators for each static member.
Parameter decorators on the constructor.
Class decorators.
If several decorators target the same declaration, the expressions are evaluated top‑down, but the resulting functions are called bottom‑up (onion model).
function foo() {
console.log('foo in');
return (target, propertyKey, descriptor) => console.log('foo out');
}
function bar() {
console.log('bar in');
return (target, propertyKey, descriptor) => console.log('bar out');
}
class A {
@foo()
@bar()
method() {}
}
// Output: foo in, bar in, bar out, foo outReflect Metadata
Basic Metadata Read/Write
reflect-metadatais an ES7 proposal that enables reading and writing metadata at declaration time. To use it, install the package and enable emitDecoratorMetadata in tsconfig.json.
import 'reflect-metadata';
@Reflect.metadata('className', 'D')
class D {
@Reflect.metadata('methodName', 'hello')
public hello(): string {
return 'hello world';
}
}
const d = new D();
console.log(Reflect.getMetadata('className', D));
console.log(Reflect.getMetadata('methodName', d));Metadata is stored on the prototype (or constructor for static members) under a hidden [[Metadata]] property that holds a Map of keys to values.
TypeScript emits built‑in metadata keys such as design:type, design:paramtypes, and design:returntype. For example, you can validate a setter’s argument type using design:type:
function validate<T>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {
const originalSet = descriptor.set!;
descriptor.set = function (value: T) {
const type = Reflect.getMetadata('design:type', target, propertyKey);
if (!(value instanceof type)) {
throw new TypeError('Invalid type.');
}
originalSet.call(this, value);
};
}
class Point { x: number; 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; }
}Inversion of Control (IoC)
Concepts: IoC, Dependency Injection, Container
IoC (Inversion of Control) is a design principle that decouples code by delegating object creation to a container.
Without IoC, a class directly instantiates its dependencies, creating tight coupling:
import { A } from './modA';
import { B } from './modB';
class C {
constructor() {
this.a = new A();
this.b = new B();
}
}With a container, C only declares the types it needs, and the container resolves and injects the instances.
import { Container } from 'injection';
import { A } from './A';
import { B } from './B';
const container = new Container();
container.bind(A);
container.bind(B);
class C {
constructor() {
this.a = container.get('a');
this.b = container.get('b');
}
}Dependency Injection (DI) is the most common IoC application: the container automatically provides required objects.
@provide()
export class UserService {
@inject()
userModel;
async getUser(userId) {
return await this.userModel.get(userId);
}
}Simple Router Implementation Based on IoC
Frameworks like NestJS and Midway use decorators to declare routes. The following example shows how @controller, @get, and @post can store path and HTTP method metadata.
export const METADATA_MAP = {
METHOD: 'method',
PATH: 'path',
GET: 'get',
POST: 'post',
MIDDLEWARE: 'middleware',
};
const { METHOD, PATH, GET, POST } = METADATA_MAP;
export const controller = (path: string): ClassDecorator => {
return target => Reflect.defineMetadata(PATH, path, target);
};
export const methodDecoCreator = (method: string) => (path: string): MethodDecorator => {
return (_target, _key, descriptor) => {
Reflect.defineMetadata(METHOD, method, descriptor.value!);
Reflect.defineMetadata(PATH, path, descriptor.value!);
};
};
const get = methodDecoCreator(GET);
const post = methodDecoCreator(POST);At runtime, a route generator reads the class‑level path and each method’s metadata, concatenates them, and builds a routing table.
const routeGenerator = (ins: Record<string, unknown>) => {
const prototype = Object.getPrototypeOf(ins);
const rootPath = Reflect.getMetadata(PATH, prototype['constructor']);
const methods = Object.getOwnPropertyNames(prototype).filter(item => item !== 'constructor');
const routeGroup = methods.map(methodName => {
const methodBody = prototype[methodName];
const path = Reflect.getMetadata(PATH, methodBody);
const method = Reflect.getMetadata(METHOD, methodBody);
return { path: `${rootPath}${path}`, method, methodName, methodBody };
});
console.log(routeGroup);
return routeGroup;
};Dependency‑Injection Libraries
Popular TypeScript DI libraries include:
TypeDI (by TypeStack)
TSyringe (by Microsoft)
Inversify (most starred)
Injection (MidwayJS team)
All of them provide two core operations: registering a class in the container (e.g., @provide or @injectable) and injecting an instance into a property (e.g., @inject).
Conclusion
After reading this article you should have a solid understanding of TypeScript decorators, metadata reflection, and how they enable IoC and DI in modern backend frameworks. Feel free to explore the compiled output in the TypeScript Playground or try a full‑stack project that heavily relies on decorators, such as Midway Serverless, TypeORM, TypeGraphQL, or util‑decorators.
Taobao Frontend Technology
The frontend landscape is constantly evolving, with rapid innovations across familiar languages. Like us, your understanding of the frontend is continually refreshed. Join us on Taobao, a vibrant, all‑encompassing platform, to uncover limitless potential.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
