A decorator is a meta-programming feature that lets you attach annotations and executable logic to classes, methods, properties, and parameters. They use the @expression syntax and run when the decorated declaration is evaluated — not when an instance is constructed.
Decorator là tính năng meta-programming cho phép bạn đính kèm annotations và logic vào classes, methods, properties và parameters. Chúng sử dụng cú pháp @expression và được thực thi tại thời điểm khai báo được đánh giá — không phải khi instance được tạo.
TypeScript 5.0 implements the Stage 3 ECMAScript Decorator Proposal with no tsconfig flag required. Older versions use experimentalDecorators, which is a different, incompatible API still required by frameworks like NestJS and TypeORM.
TypeScript 5.0 triển khai Stage 3 ECMAScript Decorator Proposal — không cần flag tsconfig. Các phiên bản cũ dùng experimentalDecorators với API khác nhau không tương thích với Stage 3.
1// tsconfig.json — TypeScript 5.0+ (Stage 3, current standard)2{3 "compilerOptions": {4 "target": "ES2022",5 "lib": ["ES2022", "DOM"]6 // No experimentalDecorators flag needed7 }8}910// tsconfig.json — Legacy decorators (NestJS, TypeORM, Inversify)11{12 "compilerOptions": {13 "target": "ES5",14 "experimentalDecorators": true,15 "emitDecoratorMetadata": true // required for Reflect.metadata16 }17}Không mix hai chế độ trong cùng một project. Kiểm tra framework của bạn trước: nếu dùng NestJS, bạn cần legacy mode cho đến khi NestJS chính thức hỗ trợ Stage 3.
A class decorator receives the target class and a ClassDecoratorContext. It can return a new class to replace the original, mutate the class in place and return void, or use context.addInitializer to run logic after the class is defined.
Class decorator nhận class target và một ClassDecoratorContext. Nó có thể trả về một class mới để thay thế class gốc, hoặc thay đổi tại chỗ và trả về void. Dùng context.addInitializer để chạy logic sau khi class được định nghĩa.
1// Sealing a class — prevents new properties being added at runtime2function sealed<T extends abstract new (...args: unknown[]) => unknown>(3 target: T,4 _context: ClassDecoratorContext5): T {6 Object.seal(target);7 Object.seal(target.prototype);8 return target;9}1011@sealed12class Config {13 host = "localhost";14 port = 3000;15}1617// Config.prototype.newMethod = () => {}; // Runtime TypeError1// Singleton — ensures only one instance can ever be created2function singleton<T extends new (...args: unknown[]) => object>(3 target: T,4 _context: ClassDecoratorContext5): T {6 let instance: InstanceType<T> | undefined;78 return new Proxy(target, {9 construct(ctor, args) {10 if (!instance) {11 instance = Reflect.construct(ctor, args) as InstanceType<T>;12 }A method decorator receives the original method and a ClassMethodDecoratorContext. To intercept calls, return a replacement function — TypeScript infers the correct type automatically from the target.
Method decorator nhận method gốc và một ClassMethodDecoratorContext. Để intercept lời gọi, trả về một hàm thay thế — TypeScript tự suy ra kiểu chính xác từ target.
1function log<This, Args extends unknown[], Return>(2 target: (this: This, ...args: Args) => Return,3 context: ClassMethodDecoratorContext<This, typeof target>4): typeof target {5 const name = String(context.name);6 return function (this: This, ...args: Args): Return {7 console.log(`[${name}] called with`, args);8 const result = target.call(this, ...args);9 console.log(`[${name}] returned`, result);10 return result;11 };12}1// Timing decorator — async-aware performance measurement2function measure<This, Args extends unknown[], Return>(3 target: (this: This, ...args: Args) => Return,4 context: ClassMethodDecoratorContext<This, typeof target>5): typeof target {6 const name = String(context.name);7 return function (this: This, ...args: Args): Return {8 const t0 = performance.now();9 const result = target.call(this, ...args);10 const duration = performance.now() - t0;11 console.log(`${name}: ${duration.toFixed(2)}ms`);12 return result;Field decorators in TS 5.x receive undefined as their target because the field does not exist when the decorator runs. They return an initializer function that transforms the initial value at construction time. Accessor decorators use the accessor keyword to intercept both get and set.
Field decorator (TS 5.x) nhận undefined làm target vì field chưa tồn tại lúc decorator chạy. Chúng trả về một initializer function để transform giá trị tại thời điểm construction. Accessor decorator dùng keyword accessor (TS 4.9+) để intercept cả get và set.
1function nonEmpty(2 _target: undefined,3 context: ClassFieldDecoratorContext4) {5 const name = String(context.name);6 return function (this: object, value: string): string {7 if (typeof value !== "string" || value.trim() === "") {8 throw new TypeError(`${name} cannot be empty`);9 }10 return value;11 };12}1// 'accessor' creates a backing private field + getter + setter2// enabling ClassAccessorDecoratorContext interception3function clamp(min: number, max: number) {4 return function <T>(5 target: ClassAccessorDecoratorTarget<T, number>,6 _context: ClassAccessorDecoratorContext<T, number>7 ): ClassAccessorDecoratorResult<T, number> {8 return {9 get(this: T): number {10 return target.get.call(this);11 },12 set(this: T, value: number): void {Parameter decorators are not part of the Stage 3 spec and only work with experimentalDecorators: true. They are the backbone of DI frameworks like NestJS and Inversify, marking constructor parameters for injection.
Parameter decorator KHÔNG có trong Stage 3 và chỉ hoạt động với experimentalDecorators: true. Chúng được dùng rộng rãi bởi các DI framework như NestJS và Inversify để đánh dấu constructor parameters cho injection.
1// Requires: experimentalDecorators: true, emitDecoratorMetadata: true2import "reflect-metadata";34const INJECT_TOKENS = Symbol("inject");56function inject(token: string) {7 return function (8 target: object,9 _propertyKey: string | symbol | undefined,10 parameterIndex: number11 ): void {12 const tokens: string[] =A decorator factory is a function that returns a decorator, allowing you to pass configuration arguments. This is the dominant pattern in production code — nearly every real-world decorator is a factory.
Decorator factory là hàm trả về một decorator, cho phép bạn truyền tham số cấu hình. Đây là pattern phổ biến nhất trong thực tế — gần như mọi decorator trong production đều là factory.
1function retry(maxAttempts: number, delayMs = 100) {2 return function <This, Args extends unknown[], Return>(3 target: (this: This, ...args: Args) => Promise<Return>,4 context: ClassMethodDecoratorContext<This, typeof target>5 ) {6 const name = String(context.name);7 return async function (this: This, ...args: Args): Promise<Return> {8 let lastError: unknown;9 for (let attempt = 1; attempt <= maxAttempts; attempt++) {10 try {11 return await target.call(this, ...args);12 } catch (err) {When multiple decorators are stacked, their factory functions run top-to-bottom, but the decorators themselves are applied bottom-to-top — the decorator closest to the declaration runs first.
Khi nhiều decorators được xếp chồng, factory functions chạy từ trên xuống, nhưng decorators được áp dụng từ dưới lên — decorator gần class nhất chạy trước.
1function outer(_t: Function, _c: ClassDecoratorContext) {2 console.log("outer applied");3}45function inner(_t: Function, _c: ClassDecoratorContext) {6 console.log("inner applied");7}89@outer // factory called 1st, decorator applied 2nd10@inner // factory called 2nd, decorator applied 1st11class Example {}1213// Output:14// inner applied15// outer applied1// Practical: memoize + readonly composed on a method2function memoize<This extends object, Args extends unknown[], Return>(3 target: (this: This, ...args: Args) => Return,4 _context: ClassMethodDecoratorContext5): typeof target {6 const cache = new Map<string, Return>();7 return function (this: This, ...args: Args): Return {8 const key = JSON.stringify(args);9 if (cache.has(key)) return cache.get(key)!;10 const result = target.call(this, ...args);11 cache.set(key, result);12 return result;@readonly @memoize means memoize transforms the original method first, then readonly wraps the already-memoized result. Reversing the order changes behaviour.Thứ tự áp dụng quan trọng: @readonly @memoize nghĩa là memoize chạy trước trên method gốc, sau đó readonly wraps kết quả đã được memoized. Đổi thứ tự sẽ thay đổi hành vi.
TypeScript 5.0+ triển khai Stage 3 decorators — không cần flag tsconfig nào
Class decorator nhận target và context; trả về class mới để thay thế hoặc void để thay đổi tại chỗ
Method decorator trả về hàm thay thế với cùng chữ ký để intercept lời gọi
Field decorator (TS 5.x) trả về initializer function để transform giá trị ban đầu tại thời điểm construction
Dùng keyword accessor để mở khóa accessor decorators — cho phép intercept cả get và set
Parameter decorators chỉ dành cho legacy mode và các DI framework như NestJS, Inversify
Decorator factory là hàm trả về decorator — đây là cách chuẩn để truyền tham số cấu hình
Decorators được áp dụng từ dưới lên; factory functions chạy từ trên xuống