Decorators in TypeScript: Usage and Common Patterns
Introduction
Decorators in TypeScript provide a powerful way to annotate and modify classes, methods, accessors, properties, and parameters at design time and runtime. They allow developers to implement cross-cutting concerns such as logging, caching, validation, dependency injection, and runtime metadata wiring without scattering boilerplate throughout an application. For intermediate developers moving from simple class-based code to more architected systems, understanding decorators unlocks cleaner, more maintainable patterns.
In this tutorial you'll learn what decorators are, how they work under the hood, and when to use them. We'll cover configuration and setup, the five kinds of decorators TypeScript supports, decorator factories, composition patterns, common use cases (validation, memoization, authorization), and performance considerations. You will also see practical examples of building small, reusable decorator utilities and tips for writing type-safe decorator-aware APIs. By the end, you'll know how to adopt decorators responsibly and avoid common pitfalls.
This guide assumes you know TypeScript basics, classes, and generics. If you want a refresher on classes and inheritance before diving deeper, see our guide on Introduction to Classes in TypeScript: Properties and Methods which revisits class syntax and behavior relevant to decorators.
Background & Context
Decorators are an experimental feature in TypeScript that build on the decorator proposal in ECMAScript. They provide a declarative surface for attaching behavior to language elements and enable patterns similar to annotations in other languages (like Java or Python decorators). Because TypeScript types are erased at runtime, decorator implementations often rely on emitted metadata or explicit type metadata via helper libraries such as reflect-metadata.
Decorators became widely used in projects that need runtime metadata (for example dependency injection containers) and frameworks (Angular historically uses decorators heavily). Importantly, decorators operate at runtime so they can change or wrap runtime values — which is different from type-level utilities such as mapped types or conditional types. If you plan to combine decorators with advanced type-level programming, reviewing resources on conditional and mapped types can be helpful; for example see our deep-dive on Recursive Conditional Types for Complex Type Manipulations or Advanced Mapped Types: Key Remapping with Conditional Types for patterns that often appear alongside decorator-driven APIs.
Key Takeaways
- What decorators are and the five kinds supported by TypeScript.
- How to configure tsconfig and use reflect-metadata for runtime type info.
- How to implement class, method, accessor, property, and parameter decorators.
- Patterns: decorator factories, composition, and higher-order decorators.
- Best practices: performance, testing, and type-safety considerations.
- Real-world applications: validation, memoization, logging, and DI.
Prerequisites & Setup
Before building decorators, enable experimental support in your tsconfig:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "es6",
"module": "commonjs"
}
}Install reflect-metadata if you need runtime type metadata (parameter or property types):
npm install reflect-metadata
Then import it at the program entrypoint (for Node or bundlers):
import 'reflect-metadata'
Note that relying on emitted metadata ties your runtime to the behavior of the TypeScript compiler and to design-time types — which is useful but has limitations when types are complex or generic. For parameter-level design you may also want to review our coverage on function parameter extraction in Deep Dive: Parameters
Main Tutorial Sections
Class decorators: basics and examples
Class decorators receive the class constructor and can return a new constructor to replace the original. Use them to annotate or patch classes, register classes in containers, or attach metadata.
Example: simple registration decorator
function Service(target: Function) {
// attach a flag used by a container
Reflect.defineMetadata('service', true, target)
}
@Service
class UserService {
getUsers() { return ['alice', 'bob'] }
}
console.log(Reflect.getMetadata('service', UserService)) // trueIf you return a class from a class decorator, remember to preserve typings for instances. That is often done by extending the original constructor. When building decorator-driven registries, consider how classes implement interfaces — for guidance, see Implementing Interfaces with Classes.
Method decorators: wrapping and altering behavior
Method decorators are useful to decorate a specific method, providing the method name and descriptor. A common pattern is to wrap the method for logging, caching, or input validation.
Example: simple timing logger
function LogTime(_: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value
descriptor.value = function(...args: any[]) {
const t0 = Date.now()
const result = original.apply(this, args)
const t1 = Date.now()
console.log(`${propertyKey} took ${t1 - t0}ms`)
return result
}
}
class PointService {
@LogTime
compute() { /* expensive work */ }
}When wrapping, preserve metadata like length and name if consumers depend on them. For complex method-level type extraction you may refer to patterns in using infer with functions in Using infer with Functions in Conditional Types.
Accessor decorators: getters/setters and stateful wiring
Accessor decorators target getters and setters and receive a PropertyDescriptor. They are useful to intercept property reads/writes, implement computed cache, or validation.
Example: cached getter
function Cached(_: any, key: string, descriptor: PropertyDescriptor) {
const getter = descriptor.get
const cacheKey = Symbol(`${key}_cache`)
descriptor.get = function() {
if (!(cacheKey in this)) this[cacheKey] = getter!.apply(this)
return this[cacheKey]
}
}
class HeavyCalc {
@Cached
get value() { /* expensive calc */ return 42 }
}Take care with lifecycle and memory — caching in instance fields can leak if not cleared. When working with readonly and ? modifiers in types, consult our guide on Advanced Mapped Types: Modifiers (+/- readonly, ?) to reason about object shapes you might be mutating.
Property decorators: metadata and validation
Property decorators receive the target and property name but not the value or descriptor. They are useful to attach metadata for later initialization or validation.
Example: validation metadata collector
function Required(target: any, propertyKey: string) {
const existing: string[] = Reflect.getMetadata('requiredProps', target) || []
existing.push(propertyKey)
Reflect.defineMetadata('requiredProps', existing, target)
}
class DTO {
@Required
name!: string
}
// later, a validation step reads 'requiredProps' to validateBecause property decorators don't get the initializer, you often combine them with class decorators or factories that run initialization logic. If you want to build dictionary-like metadata containers keyed by properties, our article on Utility Type: Record<K, T> for Dictionary Types demonstrates analogous type-level patterns.
Parameter decorators: capturing parameter metadata
Parameter decorators are called with the method name and parameter index. They allow capturing which parameters should be injected, sanitized, or validated.
Example: inject decorator that marks a param for DI
function Inject(token: string) {
return function(target: any, _methodName: string, paramIndex: number) {
const existing = Reflect.getMetadata('inject_params', target) || []
existing.push({ index: paramIndex, token })
Reflect.defineMetadata('inject_params', existing, target)
}
}
class Controller {
handler(@Inject('UserRepo') repo: any) { /* repo injected at runtime */ }
}Parameter metadata is often combined with reflection on parameter types. For type-level parameter extraction patterns, see Deep Dive: Parameters
Decorator factories and configuration patterns
A decorator factory returns the actual decorator and lets you provide configuration values.
Example: a retry decorator with options
function Retry(times = 3) {
return function(_: any, _key: string, descriptor: PropertyDescriptor) {
const orig = descriptor.value
descriptor.value = async function(...args: any[]) {
let lastErr: any
for (let i = 0; i < times; i++) {
try { return await orig.apply(this, args) }
catch (e) { lastErr = e }
}
throw lastErr
}
}
}
class ApiClient {
@Retry(5)
async fetch() { /* ... */ }
}Factory decorators make your utilities reusable and composable. Combine factories with composition helpers (higher-order decorator builders) to keep concerns separated.
Composing decorators and order of execution
Decorators are applied in a specific order: parameter decorators run first, then method/accessor/property decorators, and finally class decorators. When multiple decorators are applied to the same item, they are evaluated bottom-to-top but executed top-to-bottom (this depends on the exact decorator kind). Understanding the order is crucial when composing behaviors like authorization -> logging -> metrics.
Example: composition pattern
function Compose(...decorators: MethodDecorator[]): MethodDecorator {
return function(target, key, descriptor) {
for (const dec of decorators.reverse()) dec(target, key as any, descriptor!)
}
}
class API {
@Compose(Auth, Metrics, LogTime)
fetch() {}
}When composing, always document the expected input and side effects of each decorator. Also consider writing small unit tests for decorated behavior to prevent regressions.
Combining decorators with type-level utilities
Decorators operate at runtime, while TypeScript types are erased. However, you can design APIs where metadata collected by decorators maps to type-safe constructs. For instance, a validation decorator could register a shape mapped type representing required fields; then a type-level utility ensures compile-time correctness.
Example pattern (high-level):
- Decorators mark fields as optional/required at runtime.
- A corresponding type alias or mapped type is enforced at compile-time to reflect constraints.
This interplay often needs careful engineering: types such as unions and intersections become important when composing schemas. For guidance on unions and intersections that appear when modeling decorated data, see Union Types: Allowing a Variable to Be One of Several Types and Intersection Types: Combining Multiple Types (Practical Guide).
Debugging and testing decorators
Because decorators run at class definition time, test harnesses must import and load decorated modules to initialize metadata. When testing, isolate small decorator behaviors in unit tests by applying them to lightweight test classes and asserting metadata or behavior.
Troubleshooting tips:
- Ensure
experimentalDecoratorsandemitDecoratorMetadataare enabled. - Confirm
reflect-metadatais imported before any decorated code runs. - Use lightweight proxies to stub heavy dependencies for decorated classes.
When decorators alter prototypes or constructors, be mindful that some test runners reload modules between tests; use cleanup hooks to reset any global registries used by decorators.
Performance considerations and lazy initialization
Decorators that perform heavy work at definition time can slow startup (for example scanning annotations across many classes). Prefer storing lightweight metadata and deferring heavy processing until needed (lazy initialization). For example, rather than validating all DTOs at module load, register validation rules and run validation only when instances are created or APIs are invoked.
Example: lazy validator
function buildValidators() {
// expensive graph building deferred until first use
}
let validatorsBuilt = false
function ensureValidators() {
if (!validatorsBuilt) { buildValidators(); validatorsBuilt = true }
}
function Validate() {
return function(constructor: Function) {
Reflect.defineMetadata('validate', true, constructor)
// do not build here
}
}Another tip is to batch metadata reads and writes instead of repeated Reflect.getMetadata/defineMetadata calls across many properties. Also, watch for memory leaks when decorators attach large structures to prototypes or global registries.
Advanced Techniques
Once you are comfortable with basic decorator patterns, advance to building typed decorator frameworks and meta-programming utilities. Use decorator metadata to generate schema objects consumed by runtime validators (e.g., JSON schema), or register injection tokens in compact containers.
Advanced tip: leverage conditional and recursive types to express decorator-driven transformation results at the type level. Combining decorators with advanced mapped types lets you create typed builders that reflect runtime metadata. See our deep-dive on Recursive Mapped Types for Deep Transformations in TypeScript and Distributional Conditional Types in TypeScript: A Practical Guide to understand how to model complex transform rules.
Advanced reflective patterns often rely on infer to extract method return or parameter types when building strongly-typed decorator APIs. For conditional type inference patterns, check Using infer with Functions in Conditional Types and Using infer with Objects in Conditional Types — Practical Guide to design decorators that expose correct types to consumers.
Best Practices & Common Pitfalls
Do:
- Keep decorator logic small and declarative; delegate heavy work to runtime steps.
- Document side-effects and composition order clearly.
- Use decorator factories for configurable behaviors instead of hard-coded variants.
- Preserve original method behavior and metadata when wrapping.
Don't:
- Avoid performing network or CPU heavy tasks at module-import (decorator execution) time.
- Rely solely on emitted metadata for security-critical decisions — metadata can be spoofed.
- Mix too many concerns in a single decorator; favor composition.
Common pitfalls:
- Forgetting to import
reflect-metadatabefore decorated modules leads to undefined metadata. - Assuming types are available at runtime — you must either use emitted metadata or explicit tokens.
- Replacing constructors carelessly in class decorators can break prototype chains or instance types. If you need to transform shapes deeply, review Advanced Mapped Types: Key Remapping with Conditional Types for strategies to model type changes.
Real-World Applications
Decorators are commonly used for:
- Dependency injection containers: annotate classes and parameters to wire services.
- Validation frameworks: mark fields as required or constrained and run checks on DTOs.
- Authorization: mark methods or controllers with role requirements, used by middleware.
- Caching/memoization: apply method-level caches with invalidation policies.
- Observability: attach metrics, logging, and tracing without polluting logic.
A typical pattern is to combine decorators with a central bootstrap that scans modules for classes annotated as services/controllers and registers them into an application graph. When modelling inputs and outputs for such systems, knowing how to use literal and union types helps; see Literal Types: Exact Values as Types and our article on Union Types: Allowing a Variable to Be One of Several Types to design resilient APIs.
Conclusion & Next Steps
Decorators unlock expressive, declarative patterns in TypeScript when used thoughtfully. Start with small, well-tested utilities (logging, caching) and gradually adopt decorator factories and composition patterns. Combine runtime metadata with type-level utilities to keep your APIs ergonomic and type-safe.
Next steps:
- Build a small decorator-based logger, test it, and measure startup impact.
- Try writing a decorator factory for validation and wire it into a simple DI container.
- Deepen type-level knowledge by reading about conditional and mapped types and how they complement runtime decorators: explore articles on Recursive Conditional Types for Complex Type Manipulations and Advanced Mapped Types: Modifiers (+/- readonly, ?).
Enhanced FAQ
Q: Are decorators part of the JavaScript standard? A: Decorators are an experimental feature in both TypeScript and JavaScript proposals. TypeScript implements a version based on earlier proposals, and ECMAScript has been iterating on the specification. Relying on decorators means accepting some potential changes to the decorator proposal; however, many teams use them successfully in production.
Q: How do I get runtime type information inside a decorator?
A: Enable emitDecoratorMetadata and import reflect-metadata. Then use Reflect.getMetadata('design:paramtypes', target, propertyKey) or similar keys. Note that metadata is best-effort and may be less precise for generics or complex mapped types.
Q: Can decorators change TypeScript types? A: No — decorators run at runtime and do not alter compile-time types directly. You can design APIs where decorators register runtime metadata that mirrors a type-level contract, but the compiler won't automatically infer type changes from decorator application. To keep compile-time safety, combine decorators with explicit generic types or mapped type helpers; this pairing is discussed in resources like Differentiating Between Interfaces and Type Aliases in TypeScript which helps determine how to compose runtime metadata with type constructs.
Q: How should I test code that uses decorators? A: Unit-test decorator logic in isolation by applying it to test-specific classes and asserting metadata or behavior. For integration tests that use a decorated application, ensure any global registries or singletons used by decorators are reset between tests. If decorators replace constructors, use factories or helpers to create test instances without relying on global module loading order.
Q: When should I avoid decorators? A: Avoid decorators if they add ambiguous runtime behavior that frustrates debugging, or if you depend on strict runtime performance and decorator work runs on import. Also avoid them if your runtime environment doesn't consistently support emitted metadata. For simple projects, explicit wiring or composition might be clearer.
Q: How do decorators interact with inheritance? A: Decorators are applied to the class or its prototype where declared. Subclasses inherit methods and properties unless they override them; decorators applied to a parent class do not automatically apply to an overridden method in a subclass. When designing frameworks, decide whether decorations should be inherited and handle prototype chains carefully. For a refresher on inheritance patterns, see Class Inheritance: Extending Classes in TypeScript.
Q: Can decorators be used with functional programming styles? A: Decorators target class-style constructs and are less applicable to pure functions. For smaller utilities around functions, consider higher-order functions or wrappers. If you need similar behaviors for functions, use function factories and wrapper functions, and consult patterns in function inference from Using infer with Functions in Conditional Types for type-safe wrappers.
Q: How do I avoid metadata spoofing or security issues when using decorators? A: Treat metadata as a convenience for wiring, not a security boundary. Validate critical inputs at runtime, use server-side checks, and never trust client-provided metadata for authorization decisions. If metadata is part of external input, sanitize and validate it.
Q: Are there type-safe ways to model decorator effects? A: Yes — by pairing decorators with generic helper types and mapped types you can expose typed builder APIs. For example, a class decorator that adds registration may also export a type-level registration map using Utility Type: Record<K, T> for Dictionary Types patterns. Exploring recursive mapped types can help design typed transforms; see Recursive Mapped Types for Deep Transformations in TypeScript.
Q: What's the recommended way to introduce decorators into an existing codebase? A: Start with a single, well-documented decorator for a cross-cutting concern (like logging) and apply it in a few places. Measure startup performance and test coverage. Ensure team agreement on semantics and maintain thorough documentation for decorator effects and composition order. If you need deeper type modeling for decorator-driven constructs, consult articles on advanced mapped and conditional types, including Distributional Conditional Types in TypeScript: A Practical Guide and Advanced Mapped Types: Key Remapping with Conditional Types to make the runtime/type-level interplay safer.
