Modern examples of diverse examples of TypeScript decorators

If you’re hunting for clear, modern examples of diverse examples of TypeScript decorators, you’re in the right place. Decorators are no longer just a cute syntax trick for class nerds; they’re showing up in real-world frameworks, internal tooling, and even code quality pipelines. The challenge is that most tutorials stop at a single `@log` decorator and call it a day. This guide walks through practical, production-style examples of TypeScript decorators that you can actually steal for your own codebase. You’ll see an example of logging, validation, caching, access control, dependency injection, and more, all wired up in a way that matches how teams are building TypeScript services and frontends in 2024 and 2025. Along the way, we’ll talk about the new standardized decorators proposal, how frameworks like Angular and NestJS lean on decorators, and where decorators genuinely pay off versus when they just add noise. No theory-only lectures—just real examples, with opinionated commentary.
Written by
Jamie
Published

Before definitions or history, let’s start with actual code. When teams talk about examples of diverse examples of TypeScript decorators, they usually mean patterns like these:

  • Logging and metrics decorators around methods
  • Validation decorators on DTOs and form models
  • Caching decorators for expensive calls
  • Authorization decorators for routes and services
  • Dependency injection and configuration decorators
  • Rate limiting and retry decorators around API clients

You’ll see all of those below, written in modern TypeScript, with a bias toward patterns that survive code review in a serious codebase.

Note: TypeScript’s legacy experimental decorators are being replaced by the standardized decorators proposal backed by TC39 and documented by Microsoft at typescriptlang.org. New projects should track that syntax, but most production code in 2024 still uses the experimental style you’ll see here.


Logging and metrics: the classic example of a method decorator

When people ask for examples of diverse examples of TypeScript decorators, logging is usually the first example of a method decorator that makes sense to everyone on the team.

function LogExecutionTime(label?: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const original = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      const start = performance.now();
      try {
        const result = await original.apply(this, args);
        return result;
      } finally {
        const end = performance.now();
        const name = label || `\({target.constructor.name}.}\(propertyKey}`;
        console.info(`[Timing] \({name}: }\((end - start).toFixed(2)} ms`);
      }
    };

    return descriptor;
  };
}

class ReportService {
  @LogExecutionTime('monthly-report')
  async generateMonthlyReport(userId: string) {
    // expensive work here
  }
}

This is one of the best examples of decorators earning their keep: every method wrapped by @LogExecutionTime gets consistent metrics without copy‑pasting timing code. In a monitoring‑heavy environment, you might forward those measurements to Prometheus, Datadog, or OpenTelemetry.

For teams concerned with performance and reliability trends, the broader topic of measurement and monitoring is covered well in resources like the NIST performance engineering guidance at nist.gov.


Validation decorators on DTOs and form models

The next example of practical decorator use is validation on data transfer objects (DTOs). Instead of scattering if (!value) checks everywhere, decorators attach rules directly to class properties.

interface ValidationRule {
  propertyKey: string;
  validator: (value: any) => string | null;
}

const validationMetadata = new WeakMap<object, ValidationRule[]>();

function Required() {
  return function (target: any, propertyKey: string) {
    const rules = validationMetadata.get(target) || [];
    rules.push({
      propertyKey,
      validator: (value) =>
        value === undefined || value === null || value === ''
          ? `${propertyKey} is required`
          : null,
    });
    validationMetadata.set(target, rules);
  };
}

function MinLength(length: number) {
  return function (target: any, propertyKey: string) {
    const rules = validationMetadata.get(target) || [];
    rules.push({
      propertyKey,
      validator: (value) =>
        typeof value === 'string' && value.length < length
          ? `\({propertyKey} must be at least }\(length} characters`
          : null,
    });
    validationMetadata.set(target, rules);
  };
}

function validate(obj: any): string[] {
  const rules = validationMetadata.get(Object.getPrototypeOf(obj)) || [];
  return rules
    .map((rule) => rule.validator((obj as any)[rule.propertyKey]))
    .filter((msg): msg is string => msg !== null);
}

class RegisterUserDto {
  @Required()
  @MinLength(3)
  username!: string;

  @Required()
  @MinLength(8)
  password!: string;
}

const dto = new RegisterUserDto();
dto.username = 'ab';

console.log(validate(dto));
// [
//   'username must be at least 3 characters',
//   'password is required'
// ]

Frameworks like NestJS and Angular offer similar decorators out of the box. This pattern is one of the best examples of decorators improving readability: the constraints live right next to the fields they protect.

If you’re working with user‑facing forms in healthcare or other regulated fields, validation isn’t just nice to have; it’s part of protecting users from bad data. Organizations like the U.S. Office of the National Coordinator for Health Information Technology (healthit.gov) publish guidance on data quality and validation in clinical software, which often maps directly onto patterns like this.


Caching expensive calls with method decorators

Another set of examples of diverse examples of TypeScript decorators comes from caching. When a method is expensive but deterministic, a decorator can handle memoization.

function Memoize(ttlMs: number = 0) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const original = descriptor.value;
    const cache = new Map<string, { value: any; expiresAt: number }>();

    descriptor.value = function (...args: any[]) {
      const key = JSON.stringify(args);
      const now = Date.now();
      const cached = cache.get(key);

      if (cached && (ttlMs === 0 || cached.expiresAt > now)) {
        return cached.value;
      }

      const result = original.apply(this, args);
      const expiresAt = ttlMs === 0 ? Number.POSITIVE_INFINITY : now + ttlMs;
      cache.set(key, { value: result, expiresAt });
      return result;
    };

    return descriptor;
  };
}

class GeoService {
  @Memoize(5 * 60 * 1000) // 5 minutes
  getCityForCoordinates(lat: number, lng: number) {
    // Imagine an expensive lookup or remote API call here
  }
}

This is a clean example of a decorator that hides a cross‑cutting concern (caching) without cluttering business logic. In 2024, this pattern shows up everywhere from frontend state management to server‑side API clients.


Access control decorators for routes and services

Authorization is where decorators start to feel like real infrastructure. Many of the best examples of TypeScript decorators in production codebases are access control decorators on routes or service methods.

interface UserContext {
  id: string;
  roles: string[];
}

function getCurrentUser(): UserContext | null {
  // imagine pulling from a request context or auth token
  return { id: '123', roles: ['user'] };
}

function RequireRole(role: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const original = descriptor.value;

    descriptor.value = function (...args: any[]) {
      const user = getCurrentUser();
      if (!user || !user.roles.includes(role)) {
        throw new Error(`Forbidden: ${role} role required`);
      }
      return original.apply(this, args);
    };

    return descriptor;
  };
}

class AdminController {
  @RequireRole('admin')
  deleteUser(userId: string) {
    // delete user logic
  }
}

This pattern keeps the authorization story consistent and visible. When you scan AdminController, you immediately see which methods are locked down. In security‑sensitive domains like healthcare or finance, that clarity matters.

If you’re designing access control around protected health information or other regulated data, the security principles described by the U.S. Department of Health & Human Services at hhs.gov are worth reviewing; decorators are one of many tools to enforce those principles in code.


Dependency injection and configuration decorators

Some of the most widely used examples of diverse examples of TypeScript decorators live in dependency injection (DI) systems. The exact syntax varies by framework, but the pattern is consistent: decorators mark classes as injectable and describe how to resolve their dependencies.

Here’s a minimal DI flavor inspired by NestJS‑style decorators:

const container = new Map<any, any>();

function Injectable() {
  return function (target: any) {
    container.set(target, new target());
  };
}

function Inject(token: any) {
  return function (target: any, propertyKey: string) {
    Object.defineProperty(target, propertyKey, {
      get() {
        return container.get(token);
      },
      enumerable: true,
      configurable: true,
    });
  };
}

@Injectable()
class EmailService {
  send(to: string, subject: string, body: string) {
    // send email
  }
}

class UserNotifier {
  @Inject(EmailService)
  private emailService!: EmailService;

  notifyWelcome(userEmail: string) {
    this.emailService.send(userEmail, 'Welcome', 'Thanks for signing up');
  }
}

Real‑world frameworks add scopes, async providers, and lifecycle hooks, but the decorator idea is the same: annotate what should be injectable, and let the container handle wiring.


Rate limiting and retry decorators for API clients

Modern apps talk to a lot of external APIs, and those APIs are not always friendly. A very practical example of a decorator is a wrapper that adds retry and rate‑limit behavior.

function Retry(maxAttempts: number = 3, delayMs: number = 200) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const original = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      let attempt = 0;
      let lastError: unknown;

      while (attempt < maxAttempts) {
        try {
          return await original.apply(this, args);
        } catch (err) {
          lastError = err;
          attempt++;
          if (attempt >= maxAttempts) break;
          await new Promise((res) => setTimeout(res, delayMs));
        }
      }

      throw lastError;
    };

    return descriptor;
  };
}

class WeatherApiClient {
  @Retry(5, 500)
  async getCurrentConditions(zipCode: string) {
    // fetch from weather API
  }
}

This is one of the best examples of decorators handling resilience. You can layer this with a rate‑limit decorator that tracks calls per minute and throws or queues when limits are reached.


Property decorators for configuration and feature flags

Not all decorators wrap behavior at call time. Some examples include property decorators that configure how values are loaded, persisted, or toggled.

Here’s a simple feature flag decorator that pulls from a global config object:

const featureFlags: Record<string, boolean> = {
  'checkout.newFlow': true,
  'profile.betaLayout': false,
};

function FeatureFlag(flagKey: string) {
  return function (target: any, propertyKey: string) {
    Object.defineProperty(target, propertyKey, {
      get() {
        return featureFlags[flagKey] ?? false;
      },
      enumerable: true,
      configurable: true,
    });
  };
}

class UiExperiments {
  @FeatureFlag('checkout.newFlow')
  isNewCheckoutEnabled!: boolean;
}

const exp = new UiExperiments();
console.log(exp.isNewCheckoutEnabled); // true

This style of decorator keeps configuration concerns centralized while still making the consuming code easy to read.


Class decorators for metadata and auto‑registration

The last category in this set of examples of diverse examples of TypeScript decorators is class decorators that attach metadata or auto‑register classes with some runtime system.

A common pattern is building a lightweight HTTP routing layer:

interface RouteDefinition {
  method: 'GET' | 'POST';
  path: string;
  handlerName: string;
}

const routes: RouteDefinition[] = [];

function Controller(basePath: string) {
  return function (constructor: Function) {
    Reflect.defineMetadata('basePath', basePath, constructor);
  };
}

function Get(path: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const constructor = target.constructor;
    const basePath = Reflect.getMetadata('basePath', constructor) || '';
    routes.push({
      method: 'GET',
      path: basePath + path,
      handlerName: propertyKey,
    });
  };
}

@Controller('/users')
class UserController {
  @Get('/')
  listUsers() {
    // return list
  }
}

console.log(routes);
// [ { method: 'GET', path: '/users/', handlerName: 'listUsers' } ]

This is functionally similar to how frameworks like NestJS or Angular build routing and module metadata under the hood. It’s a clear example of decorators turning “just classes” into framework‑aware components.

Note: If you use metadata‑driven decorators like this, you’ll need the reflect-metadata shim and emitDecoratorMetadata enabled in tsconfig.json. Details are in the official TypeScript docs at typescriptlang.org.


How these patterns fit into the 2024–2025 TypeScript ecosystem

In 2024 and heading into 2025, decorators are in a strange but interesting place:

  • The TC39 decorators proposal has advanced, and TypeScript is aligning with that standard.
  • Frameworks like Angular and NestJS still rely heavily on decorators and are adapting their syntax as the standard settles.
  • Tooling around decorators—lint rules, reflection helpers, metadata libraries—continues to mature.

When you look for the best examples of TypeScript decorators to copy, focus on patterns that:

  • Encapsulate cross‑cutting concerns (logging, metrics, validation, security)
  • Stay thin and predictable (no hidden magic that surprises new team members)
  • Are easy to test without needing the decorator itself

The examples include in this article are deliberately biased toward those criteria. They’re not just “cool”; they’re the kind of decorators that survive a tough code review.


FAQ: short answers about real examples of TypeScript decorators

Q: What are some real‑world examples of TypeScript decorators used in production apps?
Teams commonly use decorators for request validation on DTOs, logging and timing around service methods, authorization checks on controllers, dependency injection wiring, caching and memoization of expensive calls, and configuration or feature‑flag access on properties. The examples of diverse examples of TypeScript decorators shown above mirror those production patterns.

Q: Can I use these examples with the new standard decorators proposal?
Yes, but some syntax changes. The high‑level ideas—wrapping methods, annotating properties, tagging classes with metadata—still apply. You’ll want to track the latest guidance from the TypeScript team at typescriptlang.org to adjust the implementation details.

Q: Are decorators safe to use in security‑sensitive code?
They’re as safe as the code you put inside them. For access control, input validation, and logging around sensitive operations, decorators can actually improve safety by centralizing logic. Just avoid hiding complex or risky behavior behind cute syntax. For broader security practices, the OWASP Foundation at owasp.org is a good reference.

Q: Do decorators hurt performance?
There is overhead, because they add another function call layer and sometimes extra bookkeeping. In most web and API services, that cost is small compared with network and I/O latency. If you’re writing ultra‑low‑latency code, measure the impact with and without decorators and decide based on data.

Q: How do I decide whether to introduce a decorator versus a plain helper function?
Use a decorator when you want a concern to be visually and structurally attached to a class, method, or property—logging around a method, validation on a field, or a role requirement on a route. Use a plain helper when the logic is tightly coupled to a single call site or would be confusing if hidden behind annotation syntax.


If you’re evaluating examples of diverse examples of TypeScript decorators for your own stack, start with one or two concerns—typically logging and validation—and see how they affect readability and testability. Decorators are a tool, not a religion; the best examples are the ones that make your codebase easier to reason about six months from now.

Explore More TypeScript Code Snippets

Discover more examples and insights in this category.

View All TypeScript Code Snippets