- TypeScript Tutorial
- TypeScript Home
- TypeScript Introduction
- TypeScript Setup
- TypeScript First Program
- TypeScript vs JavaScript
- TypeScript Data Types
- TypeScript Type Inference
- TypeScript Type Annotations
- TypeScript Interfaces
- TypeScript Enums
- TypeScript Type Aliases
- TypeScript Type Assertions
- TypeScript Variables
- TypeScript Functions
- TypeScript Functions
- TypeScript Optional Parameters
- TypeScript Default Parameters
- TypeScript Rest Parameters
- TypeScript Arrow Functions
- Classes and Objects
- Introduction to Classes
- Properties and Methods
- Access Modifiers
- Static Members
- Inheritance
- Abstract Classes
- Interfaces vs Classes
- Advanced Types
- TypeScript Union Types
- TypeScript Intersection Types
- TypeScript Literal Types
- TypeScript Nullable Types
- TypeScript Type Guards
- TypeScript Discriminated Unions
- TypeScript Index Signatures
- TypeScript Generics
- Introduction to Generics
- TypeScript Generic Functions
- TypeScript Generic Classes
- TypeScript Generic Constraints
- TypeScript Modules
- Introduction to Modules
- TypeScript Import and Export
- TypeScript Default Exports
- TypeScript Namespace
- Decorators
- Introduction to Decorators
- TypeScript Class Decorators
- TypeScript Method Decorators
- TypeScript Property Decorators
- TypeScript Parameter Decorators
- Configuration
- TypeScript tsconfig.json File
- TypeScript Compiler Options
- TypeScript Strict Mode
- TypeScript Watch Mode
TypeScript Class Decorators
Class decorators in TypeScript are a powerful feature that allows you to observe, modify, or replace a class definition. Think of them as a "wrapper" for your class. When you apply a decorator to a class, you are essentially passing the class's constructor through a function that can tweak its behavior before any instances are even created.
While decorators are technically an "experimental" feature in TypeScript, they are the backbone of modern frameworks like Angular, NestJS, and TypeORM. They allow developers to use a declarative programming style, making code cleaner and more expressive by separating cross-cutting concerns (like logging or validation) from the core logic of the class.
What is a Class Decorator?
A class decorator is simply a function. This function receives one specific argument: the constructor of the class being decorated. Once you have access to this constructor, you can modify its prototype to add new methods, record metadata for a framework to read later, or even return a completely different constructor to override the original class logic.
In the TypeScript ecosystem, class decorators are identified by the @ symbol. They are placed immediately above the class declaration they are intended to modify.
Enabling Class Decorators in TypeScript
Because decorators are still considered experimental by the TypeScript team, they are disabled by default. If you try to use them without configuration, the compiler will throw an error.
Example: Enabling Class Decorators
To enable them, you must update your tsconfig.json file by setting experimentalDecorators to true within the compilerOptions block.
{
"compilerOptions": {
"target": "ES6",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
- The
experimentalDecoratorsflag tells the compiler to support the@syntax. - The
emitDecoratorMetadataflag is often used alongside decorators to allow frameworks (like NestJS) to "read" the types of your constructor arguments at runtime.
tsconfig.json. Changes to this file are usually not picked up automatically by hot-reloaders.
Syntax of Class Decorators
At its core, a class decorator function looks like this:
function MyClassDecorator(constructor: Function) {
// Do something with the constructor
console.log("Class definition found:", constructor.name);
}
In this snippet, constructor represents the class itself. You can inspect its name, look at its prototype, or add static properties to it.
Applying Class Decorators
To use your decorator, place it directly above your class. You don't need to call it like a function (unless you're using a factory, which we'll cover later); the @ symbol handles the execution for you.
Example: Applying a Class Decorator
function Reportable(target: Function) {
target.prototype.reportedAt = new Date();
}
@Reportable
class User {
constructor(public name: string) {}
}
const user = new User('Alice');
// We have to cast to 'any' here because TypeScript doesn't know about
// properties added via prototype modification at compile time.
console.log((user as any).reportedAt);
- The
Reportabledecorator attaches areportedAtproperty to the class prototype. - Every instance of
Userwill now have access to this timestamp, even though it wasn't defined in the original class body.
Modifying the Class Behavior
While adding properties to a prototype is useful, decorators are most powerful when they modify how a class behaves. You can use them to wrap existing methods or add utility functions that your application requires globally.
Example: Adding a Helper Method via Decorator
function Timestamped(target: Function) {
target.prototype.getCreationDate = function() {
return new Date();
};
}
@Timestamped
class Document {
constructor(public title: string) {}
}
const doc = new Document('Monthly Report');
console.log((doc as any).getCreationDate());
This approach is common in older libraries to "mix in" functionality. However, modern TypeScript developers often prefer Constructor Inheritance within decorators to ensure better type safety.
Replacing the Class Constructor
A class decorator can actually replace the original constructor with a new one. This is the most advanced use case. To do this, your decorator must return a new constructor function (or a new class) that extends the original.
Example: Replacing the Constructor
function Frozen(constructor: Function) {
Object.freeze(constructor);
Object.freeze(constructor.prototype);
}
@Frozen
class Config {
static API_URL = "https://api.example.com";
}
// Config.API_URL = "http://hacked.com"; // This would fail at runtime because the class is frozen.
If you want to modify the instantiation logic, you can return a class that extends the original:
function WithDefaultRole<T extends { new (...args: any[]): {} }>(Base: T) {
return class extends Base {
role = "Guest";
createdAt = new Date();
};
}
@WithDefaultRole
class Profile {
constructor(public username: string) {}
}
const myProfile = new Profile("jdoe");
console.log((myProfile as any).role); // Output: Guest
extends Base) to ensure that the instanceof checks and existing properties still work as expected.
Class Decorator with Parameters
Sometimes you need to pass configuration data to your decorator. To do this, you use a Decorator Factory. This is a function that returns the actual decorator function.
Example: Class Decorator Factory
function Component(options: { selector: string, template: string }) {
return function(constructor: Function) {
constructor.prototype.selector = options.selector;
constructor.prototype.template = options.template;
console.log(`Registered component: ${options.selector}`);
};
}
@Component({
selector: 'app-root',
template: '<h1>Hello World</h1>'
})
class AppComponent {}
- The
Componentfunction is the factory. It takes anoptionsobject. - It returns an anonymous function which acts as the decorator, applying those options to the class.
Using Class Decorators for Dependency Injection (DI)
In high-level architecture, decorators act as "markers." A central container looks for these markers to decide how to manage the class. This is exactly how frameworks like NestJS or TypeDI work.
Example: Simulated Dependency Injection Registry
const registry = new Map<string, any>();
function Service(constructor: Function) {
// Map the class name to its constructor for later instantiation
registry.set(constructor.name, new (constructor as any)());
}
@Service
class DatabaseService {
query(sql: string) {
console.log(`Executing: ${sql}`);
}
}
// Later in the app, we can fetch the instance without manually calling 'new'
const db = registry.get('DatabaseService');
db.query("SELECT * FROM users");
Summary
- Class decorators act as wrappers that allow you to modify or annotate classes at the point of definition.
- They receive the constructor function as their primary argument.
- You must enable
experimentalDecoratorsin your tsconfig.json to use them. - Decorator Factories allow you to pass custom configuration and parameters into your decorators.
- They are ideal for cross-cutting concerns like logging, dependency injection, and metadata registration, keeping your business logic clean.