Decorators in TypeScript: The Complete Practical Guide
TC39 decorators are now stable in TypeScript 5.0+. Learn how to use class, method, and field decorators with practical examples for logging, validation, and dependency injection.
Decorators have had a complicated history in TypeScript. The experimental decorators (enabled with --experimentalDecorators) have been around for years, but they were based on a legacy proposal that never advanced. As of TypeScript 5.0, the TC39 Stage 3 decorators are fully supported — and they work differently. Here's your complete guide to using them in production.
The New Decorator Syntax
TC39 decorators are functions that receive the decorated value and a context object. They can optionally return a replacement value:
// A simple method decorator that logs calls
function log<T extends (...args: unknown[]) => unknown>(
target: T,
context: ClassMethodDecoratorContext
): T {
const methodName = String(context.name);
return function (this: unknown, ...args: unknown[]) {
console.log(`Calling ${methodName} with`, args);
const result = target.apply(this, args);
console.log(`${methodName} returned`, result);
return result;
} as T;
}
class UserService {
@log
async findById(id: string): Promise<User | null> {
return prisma.user.findUnique({ where: { id } });
}
@log
async updateName(id: string, name: string): Promise<User> {
return prisma.user.update({ where: { id }, data: { name } });
}
}
// Console output:
// Calling findById with ["abc-123"]
// findById returned { id: "abc-123", name: "Alice" }
Key difference from legacy decorators: the new API receives the value being decorated (not a property descriptor), and uses a typed context object instead of target/key/descriptor arguments.
Class Decorators: Adding Metadata
Class decorators receive the class constructor and can return a subclass or modify it in place:
// Register a class in a service container
const registry = new Map<string, new (...args: unknown[]) => unknown>();
function injectable(target: new (...args: unknown[]) => unknown, context: ClassDecoratorContext) {
const name = String(context.name);
registry.set(name, target);
console.log(`Registered service: ${name}`);
}
@injectable
class EmailService {
async send(to: string, subject: string, body: string) {
// Send email via Resend
}
}
@injectable
class NotificationService {
constructor(private email: EmailService) {}
async notify(userId: string, message: string) {
await this.email.send(userId, "Notification", message);
}
}
// Later: resolve from registry
const EmailSvc = registry.get("EmailService")!;
const instance = new EmailSvc();
Field Decorators and Accessor Decorators
Field decorators use the accessor keyword (auto-accessor fields) to intercept get/set operations — perfect for validation:
function minLength(min: number) {
return function <T extends string>(
_target: ClassAccessorDecoratorTarget<unknown, T>,
context: ClassAccessorDecoratorContext<unknown, T>
): ClassAccessorDecoratorResult<unknown, T> {
return {
set(value: T) {
if (value.length < min) {
throw new Error(
`${String(context.name)} must be at least ${min} characters`
);
}
context.access.set(this, value);
},
};
};
}
function range(min: number, max: number) {
return function (
_target: ClassAccessorDecoratorTarget<unknown, number>,
context: ClassAccessorDecoratorContext<unknown, number>
): ClassAccessorDecoratorResult<unknown, number> {
return {
set(value: number) {
if (value < min || value > max) {
throw new Error(
`${String(context.name)} must be between ${min} and ${max}`
);
}
context.access.set(this, value);
},
};
};
}
class BlogPost {
@minLength(3)
accessor title: string = "";
@range(1, 60)
accessor readingTime: number = 5;
}
const post = new BlogPost();
post.title = "Hi"; // ❌ Error: title must be at least 3 characters
post.readingTime = 100; // ❌ Error: readingTime must be between 1 and 60
Practical Pattern: Timing Decorator
A performance-measuring decorator that works great during development:
function timed<T extends (...args: unknown[]) => unknown>(
target: T,
context: ClassMethodDecoratorContext
): T {
const name = String(context.name);
return function (this: unknown, ...args: unknown[]) {
const start = performance.now();
const result = target.apply(this, args);
// Handle both sync and async
if (result instanceof Promise) {
return result.then((val) => {
console.log(`${name} took ${(performance.now() - start).toFixed(2)}ms`);
return val;
});
}
console.log(`${name} took ${(performance.now() - start).toFixed(2)}ms`);
return result;
} as T;
}
Migration from Legacy Decorators
If you're migrating from --experimentalDecorators, the key changes are:
- No more
reflect-metadata— usecontext.metadatainstead - Method decorators receive the function directly, not a property descriptor
- Field decorators require the
accessorkeyword for get/set interception - Parameter decorators are not supported in the TC39 proposal
Takeaways
- Use TC39 decorators (no
--experimentalDecoratorsflag) for all new code - Method decorators are ideal for cross-cutting concerns: logging, timing, caching, auth checks
- Field validation with
accessordecorators replaces manual getter/setter boilerplate - Class decorators enable lightweight dependency injection and service registration
- Keep decorators focused and composable — each should do one thing
Decorators bring the declarative, aspect-oriented programming style that frameworks like Angular and NestJS have relied on for years — but now with a standard, stable foundation that the entire ecosystem can build on.
Admin
Cal.com
Open source scheduling — tự host booking system, thay thế Calendly. Free & privacy-first.
Bình luận (0)
Đăng nhập để bình luận
Chưa có bình luận nào. Hãy là người đầu tiên!