TypeScript 5 Decorators & Metaprogramming: From Principles to a Complete Dependency Injection Framework
前端工程(Updated Jun 2, 2026)
The Evolution of Decorators
Stage 2 (Legacy, TypeScript 4)
// Legacy: decorator is a function that receives (target, key, descriptor)
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${key} with`, args);
return original.apply(this, args);
};
}
Stage 3 (New, TypeScript 5+)
// New: decorator is a function that returns a function
function Log(
value: Function,
context: ClassMethodDecoratorContext
) {
return function (this: any, ...args: any[]) {
console.log(`Calling ${String(context.name)} with`, args);
return value.apply(this, args);
};
}
Key Differences
| Aspect | Stage 2 (Legacy) | Stage 3 (New) |
|---|---|---|
| Parameters | (target, key, descriptor) |
(value, context) |
| Return Value | Modify descriptor | Return replacement value |
| Metadata | Requires reflect-metadata |
Native context.metadata |
| Class Decorator | Modify constructor | Return new constructor |
| Execution Order | Inside to outside | Outside to inside |
| TC39 Status | Deprecated | Stage 3 |
Five Decorator Types
1. Class Decorators
function sealed<T extends { new(...args: any[]): {} }>(
target: T,
context: ClassDecoratorContext
) {
Object.seal(target);
Object.seal(target.prototype);
return target;
}
@sealed
class Config {
static apiKey = 'xxx';
}
2. Method Decorators
function debounce(delay: number) {
return function (
originalMethod: Function,
context: ClassMethodDecoratorContext
) {
let timer: ReturnType<typeof setTimeout>;
return function (this: any, ...args: any[]) {
clearTimeout(timer);
timer = setTimeout(() => originalMethod.apply(this, args), delay);
};
};
}
class SearchBox {
@debounce(300)
onInput(text: string) {
fetchSuggestions(text);
}
}
3. Property Decorators
function required(
value: undefined,
context: ClassFieldDecoratorContext
) {
return function (this: any, initialValue: any) {
if (initialValue === undefined || initialValue === null) {
throw new Error(`${String(context.name)} is required`);
}
return initialValue;
};
}
class User {
@required name!: string;
@required email!: string;
}
4. Getter/Setter Decorators
function format(template: string) {
return function (
target: Function,
context: ClassGetterDecoratorContext
) {
return function (this: any) {
const raw = target.call(this);
return template.replace('{}', String(raw));
};
};
}
class Product {
#price: number;
@format('${}')
get price() { return this.#price; }
}
5. Auto-Accessor Decorators
function logged(
target: ClassAccessorDecoratorTarget<any, any>,
context: ClassAccessorDecoratorContext
): ClassAccessorDecoratorResult<any, any> {
return {
get(this: any) {
const value = target.get.call(this);
console.log(`Getting ${String(context.name)}: ${value}`);
return value;
},
set(this: any, newValue: any) {
console.log(`Setting ${String(context.name)}: ${newValue}`);
target.set.call(this, newValue);
},
};
}
class Settings {
@logged accessor theme = 'light';
}
Metadata Reflection
reflect-metadata
import 'reflect-metadata';
// Define metadata
Reflect.defineMetadata('role', 'admin', UserController);
Reflect.defineMetadata('route', '/api/users', UserController.prototype, 'getUsers');
// Read metadata
const role = Reflect.getMetadata('role', UserController); // 'admin'
const route = Reflect.getMetadata('route', UserController.prototype, 'getUsers'); // '/api/users'
Decorators + Metadata = Declarative Programming
import 'reflect-metadata';
const ROUTE_KEY = Symbol('route');
const METHOD_KEY = Symbol('method');
function Get(path: string) {
return function (target: Function, context: ClassMethodDecoratorContext) {
Reflect.defineMetadata(ROUTE_KEY, path, context.static);
Reflect.defineMetadata(METHOD_KEY, 'GET', context.static);
};
}
function Controller(prefix: string) {
return function (target: Function, context: ClassDecoratorContext) {
Reflect.defineMetadata('prefix', prefix, target);
};
}
@Controller('/api/users')
class UserController {
@Get('/')
list() { return [{ id: 1, name: 'Alice' }]; }
@Get('/:id')
detail() { return { id: 1, name: 'Alice' }; }
}
// Auto-register routes
function registerRoutes(controller: any) {
const prefix = Reflect.getMetadata('prefix', controller.constructor);
const methods = Object.getOwnPropertyNames(controller.constructor.prototype);
methods.forEach(method => {
const route = Reflect.getMetadata(ROUTE_KEY, controller.constructor.prototype, method);
if (route) {
const fullPath = prefix + route;
console.log(`Register: GET ${fullPath} → ${method}`);
}
});
}
Hands-On: Building an IoC Container from Scratch
import 'reflect-metadata';
const INJECT_KEY = Symbol('inject');
// @Injectable decorator: marks injectable services
function Injectable(target: Function, context: ClassDecoratorContext) {
Reflect.defineMetadata('injectable', true, target);
return target;
}
// @Inject decorator: declares dependencies
function Inject(token: string) {
return function (value: undefined, context: ClassFieldDecoratorContext) {
return function (this: any) {
return Container.resolve(token);
};
};
}
// IoC Container
class Container {
private static instances = new Map<string, any>();
private static registrations = new Map<string, Function>();
static register(token: string, target: Function) {
this.registrations.set(token, target);
}
static resolve<T>(token: string): T {
if (this.instances.has(token)) {
return this.instances.get(token);
}
const Target = this.registrations.get(token);
if (!Target) throw new Error(`No registration for ${token}`);
const instance = new (Target as any)();
this.instances.set(token, instance);
return instance;
}
}
// Usage
@Injectable
class Logger {
log(msg: string) { console.log(`[LOG] ${msg}`); }
}
@Injectable
class Database {
@Inject('Logger') logger!: Logger;
query(sql: string) {
this.logger.log(`Query: ${sql}`);
return [{ id: 1 }];
}
}
Container.register('Logger', Logger);
Container.register('Database', Database);
const db = Container.resolve<Database>('Database');
db.query('SELECT * FROM users');
Hands-On: ORM Property Mapping
import 'reflect-metadata';
const COLUMN_KEY = Symbol('column');
function Column(name: string, options?: { type?: string; primary?: boolean }) {
return function (value: undefined, context: ClassFieldDecoratorContext) {
Reflect.defineMetadata(COLUMN_KEY, { name, ...options }, context.static);
};
}
function Entity(table: string) {
return function (target: Function, context: ClassDecoratorContext) {
Reflect.defineMetadata('table', table, target);
};
}
@Entity('users')
class User {
@Column('id', { primary: true })
id!: number;
@Column('username')
name!: string;
@Column('created_at', { type: 'datetime' })
createdAt!: Date;
}
// Auto-generate SQL
function toSQL(entity: Function): string {
const table = Reflect.getMetadata('table', entity);
const fields: string[] = [];
const prototype = entity.prototype;
for (const key of Object.getOwnPropertyNames(prototype)) {
const col = Reflect.getMetadata(COLUMN_KEY, prototype, key);
if (col) {
fields.push(`${col.name} ${col.type || 'VARCHAR(255)'}`);
}
}
return `CREATE TABLE ${table} (${fields.join(', ')});`;
}
Hands-On: Validation Framework
import 'reflect-metadata';
const VALIDATE_KEY = Symbol('validate');
function validate(validator: (value: any) => boolean, message: string) {
return function (value: undefined, context: ClassFieldDecoratorContext) {
const existing = Reflect.getMetadata(VALIDATE_KEY, context.static) || [];
existing.push({ validator, message, field: context.name });
Reflect.defineMetadata(VALIDATE_KEY, existing, context.static);
};
}
const Min = (n: number) => validate(v => v >= n, `Must be at least ${n}`);
const Max = (n: number) => validate(v => v <= n, `Must be at most ${n}`);
const Email = () => validate(v => /^[^@]+@[^@]+$/.test(v), 'Invalid email');
const Required = () => validate(v => v != null, 'Is required');
class CreateUserDTO {
@Required() @Min(1) id!: number;
@Required() @Email() email!: string;
@Required() @Min(0) @Max(150) age!: number;
}
function validateDto(instance: any): string[] {
const rules = Reflect.getMetadata(VALIDATE_KEY, instance.constructor) || [];
const errors: string[] = [];
for (const rule of rules) {
const value = instance[rule.field];
if (!rule.validator(value)) {
errors.push(`${String(rule.field)} ${rule.message}`);
}
}
return errors;
}
Decorator Factory Pattern
// Decorator factory: receives parameters, returns a decorator
function Cache(ttl: number) {
const cache = new Map<string, { value: any; expiry: number }>();
return function (originalMethod: Function, context: ClassMethodDecoratorContext) {
return function (this: any, ...args: any[]) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && Date.now() < cached.expiry) {
return cached.value;
}
const result = originalMethod.apply(this, args);
cache.set(key, { value: result, expiry: Date.now() + ttl * 1000 });
return result;
};
};
}
class ApiService {
@Cache(60) // Cache for 60 seconds
async getUsers() {
return fetch('/api/users').then(r => r.json());
}
}
TypeScript Configuration
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": false,
"emitDecoratorMetadata": true
}
}
| Option | Description |
|---|---|
experimentalDecorators: false |
Use Stage 3 new decorators |
experimentalDecorators: true |
Use legacy decorators |
emitDecoratorMetadata: true |
Emit type metadata (requires reflect-metadata) |
Summary
TypeScript 5's Stage 3 decorators represent a standardization milestone for metaprogramming. Combined with reflect-metadata, decorators become a powerful tool for declarative programming — IoC containers, ORM mappings, validation frameworks, and route registration can all be elegantly implemented with decorators. The core idea: declare "what to do" in your code, and encapsulate "how to do it" inside the decorator.
Try these browser-local tools — no sign-up required →
#TypeScript#装饰器#元编程#Reflect.metadata#依赖注入