Decorators—Add Metadata and Behavior to Classes
Use decorators to attach metadata and inject reusable behavior into classes, methods, and properties in a clean, declarative way.
As developers, we often need to inject behavior, annotate structure, or enforce contracts without polluting our core logic. Think of logging, validation, dependency injection, or marking fields as required. These are cross-cutting concerns. They crop up everywhere—classes, methods, properties—repeating the same logic.
Enter decorators.
Decorators in TypeScript provide a powerful syntax to extend and annotate code behavior at runtime—while keeping business logic crisp and uncluttered. If you’ve worked with Angular or NestJS, you’ve already seen them: @Component()
, @Injectable()
, @Get()
, and more.
This lesson introduces decorators from first principles: what they are, how to enable them, how they work under the hood, and how to apply them responsibly. We’ll walk through class, method, and property decorators using plain TypeScript—no frameworks—to clearly understand what’s happening.
Let’s get started by laying the foundation: how to work with class constructors as values.
Understanding constructor types
Decorators operate at definition time. When we apply a decorator to a class, what we’re really doing is passing the class’s constructor function to another function. To build decorators well, we need to know how to type constructor functions precisely.
In TypeScript, a constructor type looks like this:
type AnyConstructor = { new (...args: any[]): {} };
This reads: “A value that can be called with new
, takes any number of arguments, and returns an object.”
Let’s see that type in action.
type AnyConstructor = { new (...args: any[]): {} };class Example {constructor(public id: number) {}}function makeInstance(Ctor: AnyConstructor, ...args: any[]) {return new Ctor(...args);}const instance = makeInstance(Example, 42);console.log(instance);
Explanation:
Line 1: We define a type alias
AnyConstructor
to represent any class that can be instantiated usingnew
, with any number of arguments, and returns an object.Line 3: The
Example
class matches this type because it has a constructor that can be called withnew
and returns an instance.Line 6: The
makeInstance
function takes a class constructor and creates a new instance by calling it with the provided arguments.Line 9: We pass the
Example
class tomakeInstance
, along with the argument42
, and get back a newExample
instance.
This forms the foundation. Decorators receive class constructors, and knowing how to type them safely is the first step.
Enabling decorators in TypeScript
Decorators are officially supported in TypeScript, but they are still classified as experimental under the current JavaScript specification.
To use decorators today, you must explicitly opt in by enabling the experimentalDecorators
flag in your tsconfig.json
. Without this flag, decorator syntax will result in a compiler error, even in the latest TypeScript versions. ...