Bỏ qua để vào nội dung chính
Decorators in TypeScript: The Complete Practical Guide

Decorators in TypeScript: The Complete Practical Guide

Bởi Charlotte Adams
06 thg 3, 20267 phút đọc

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 — use context.metadata instead
  • Method decorators receive the function directly, not a property descriptor
  • Field decorators require the accessor keyword for get/set interception
  • Parameter decorators are not supported in the TC39 proposal

Takeaways

  • Use TC39 decorators (no --experimentalDecorators flag) for all new code
  • Method decorators are ideal for cross-cutting concerns: logging, timing, caching, auth checks
  • Field validation with accessor decorators 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.

Không spam, hủy đăng ký bất kỳ lúc nào.

Bài viết liên quan