Decorators & Metadata

TypeScript Intermediate — Decorators & Metadata
TypeScript Intermediate
Course 2 · Chapter 3 · Decorators & Metadata

✨ Decorators & Metadata

Decorators are TypeScript's way of adding metadata and behavior to classes, methods, properties, and parameters. They're powerful (and experimental), enabling frameworks like Angular to work their magic. This chapter teaches you how to read, write, and reason about decorators.
⚠️ Note: Decorators are an experimental feature. To use them, enable "experimentalDecorators": true in tsconfig.json. They're currently a TC39 proposal but already widely used in production.

What Are Decorators?

A decorator is a function that modifies a class, method, property, or parameter. It runs at class definition time and can inspect or alter the target. Decorators are prefixed with @:

@Log class User { @Validate name: string; @Deprecated oldMethod() { } } // Decorators are syntactic sugar for function wrapping: // @Log class User { } is equivalent to: class User { } User = Log(User);

Key insight: A decorator is just a function that receives the target (class, method, etc.) and can return a modified version of it.

🏛️ Class Decorators

A class decorator receives the constructor function and can replace or enhance it:

function Sealed(constructor: Function) { Object.seal(constructor); Object.seal(constructor.prototype); } @Sealed class User { name = "Alice"; } // Now you can't add properties to User or User.prototype // User.newProp = 123; // ❌ Error

Practical: Class Decorator for Logging

function Log(constructor: Function) { return class extends constructor { constructor(...args: any[]) { console.log(`Creating instance of ${constructor.name}`); super(...args); } }; } @Log class Database { constructor(url: string) { console.log(`Connecting to ${url}`); } } new Database("postgres://..."); // Output: Creating instance of Database // Connecting to postgres://...

🏷️ Property Decorators

A property decorator receives the class prototype and the property name. It can't return anything (it modifies in place):

function Uppercase(target: any, propertyKey: string) { let value: string; const getter = () => value; const setter = (newValue: string) => { value = newValue.toUpperCase(); }; Object.defineProperty(target, propertyKey, { get: getter, set: setter, enumerable: true, configurable: true }); } class User { @Uppercase name: string = ""; } const user = new User(); user.name = "alice"; console.log(user.name); // "ALICE" ✅

Signature

(target: any, propertyKey: string)

Use Cases

Validation, transformation, metadata attachment, lazy initialization.

Limitation

Can't return anything. Modifies the class prototype in place.

Runs When?

At class definition time, not when instances are created.

🎯 Method Decorators

A method decorator receives the prototype, method name, and a property descriptor. It can wrap or replace the method:

function TimeIt(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function(...args: any[]) { const start = Date.now(); const result = originalMethod.apply(this, args); const elapsed = Date.now() - start; console.log(`${propertyKey} took ${elapsed}ms`); return result; }; return descriptor; } class Calculator { @TimeIt fibonacci(n: number): number { if (n <= 1) return n; return this.fibonacci(n - 1) + this.fibonacci(n - 2); } } const calc = new Calculator(); calc.fibonacci(10); // Logs execution time

Signature

(target: any, propertyKey: string, descriptor: PropertyDescriptor)

Descriptor.value

The original method function. Replace or wrap it to modify behavior.

Common Patterns

Logging, timing, caching, error handling, authentication checks.

Return Value

Return the (modified) descriptor to finalize the change.

🔗 Parameter Decorators

A parameter decorator receives the prototype, method name, and parameter index. Typically used for metadata (requires reflecting metadata):

function Required(target: any, propertyKey: string | undefined, parameterIndex: number) { // Store metadata about this parameter const existingMetadata = Reflect.getOwnMetadata("required", target, propertyKey) || []; existingMetadata[parameterIndex] = true; Reflect.defineMetadata("required", existingMetadata, target, propertyKey); } class User { create(@Required name: string, @Required email: string) { console.log(`Creating user: ${name} (${email})`); } } // Later, you could validate based on the metadata const metadata = Reflect.getOwnMetadata("required", User.prototype, "create"); // metadata is [true, true] — both parameters are required
Note: Parameter decorators alone can't enforce behavior. They're typically used with method decorators that read the metadata and validate.

⚙️ Decorator Factories: Parameterized Decorators

To pass options to a decorator, wrap it in a factory function:

function MinLength(min: number) { return function(target: any, propertyKey: string) { let value: string; const setter = (newValue: string) => { if (newValue.length < min) { throw new Error(`${propertyKey} must be at least ${min} characters`); } value = newValue; }; Object.defineProperty(target, propertyKey, { set: setter }); }; } class User { @MinLength(3) name: string = ""; } const user = new User(); user.name = "ab"; // ❌ Error: must be at least 3 characters

🏗️ Real-World Pattern: Validation Decorator

Complete Validation System

function Validate(rules: { [key: string]: (value: any) => boolean }) { return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function(...args: any[]) { for (const [param, rule] of Object.entries(rules)) { const value = args[0]; if (!rule(value)) { throw new Error(`Validation failed for ${param}`); } } return originalMethod.apply(this, args); }; return descriptor; }; } class UserService { @Validate({ email: (v) => typeof v === "string" && v.includes("@") }) createUser(email: string) { console.log(`User created: ${email}`); } } const service = new UserService(); service.createUser("alice@example.com"); // ✅ service.createUser("invalid"); // ❌ Error: Validation failed

💻 Coding Challenges

Challenge 1: Method Logging Decorator

Create a method decorator that logs when a method is called and what it returns. Apply it to a sample class method.

Goal: Practice method decorators and descriptor manipulation.

→ Solution

Challenge 2: Property Validator Decorator

Create a property decorator that enforces a constraint (e.g., email must be a valid format). Use it on a class property and test it.

Goal: Build property decorators with validation logic.

→ Solution

Challenge 3: Decorator Factory

Create a decorator factory (decorator with parameters) that limits method calls to N times. Apply it to a method and verify it stops after the limit.

Goal: Understand how decorator factories work and parameterize decorators.

→ Solution

⚠️ Gotcha: Decorators Run at Definition Time

Decorators execute when the class is defined, not when instances are created. If you need per-instance behavior, wrap the original method inside a decorator factory or use a method decorator that returns a wrapped function. Also, always remember: TypeScript's decorator output depends on your tsconfig settings — test with experimentalDecorators enabled.

🎯 What's Next

With decorators mastered, we'll explore Async Patterns — mastering Promises, async/await, error handling, and concurrent operations for real-world applications.