Decorator là một hàm bạn gắn vào class hoặc thành viên của class bằng @name. Nó nhận phương thức gốc (hoặc class, hoặc thuộc tính) cùng một đối tượng context mô tả thứ đó, và có thể trả về một thứ thay thế. TypeScript hỗ trợ decorator chuẩn mà không cần cờ biên dịch nào.
@logged chạy một lần, khi class được định nghĩa, và thay add bằng hàm bao mà nó trả về. Mọi lời gọi đều đi qua hàm bao đó. Các tham số generic giữ nguyên kiểu của this, của đối số và của giá trị trả về, nên add vẫn nhận hai số và trả về một số.
Decorator được biên dịch thế nào
Decorator chuẩn đến từ một đề xuất TC39 cho JavaScript, và TypeScript đã hiện thực chúng từ TypeScript 5.0. Đề xuất này chưa thuộc chuẩn JavaScript, và Node 24 không phân tích được cú pháp @, nên khi target là ES2022 (như trên các trang này), trình biên dịch viết lại mỗi class có decorator thành JavaScript thuần gọi đến các hàm helper (__esDecorate và __runInitializers, được sinh ra ở đầu tệp). Kết quả chạy được ở mọi nơi chạy được ES2022.
Một hệ quả: tệp có decorator không chạy được với cơ chế bỏ kiểu có sẵn của Node (node file.ts), vì cơ chế này chỉ xóa kiểu và để nguyên @. Node dừng lại với SyntaxError: Invalid or unexpected token. Hãy biên dịch bằng tsc hoặc một bundler trước.
Các loại decorator và chữ ký của chúng
Mọi decorator chuẩn đều có dạng (value, context) => replacement | void. value là gì và bạn được trả về gì phụ thuộc vào thứ được trang trí:
| Trang trí | value | Kiểu context | Trả về |
|---|---|---|---|
| class | chính class đó | ClassDecoratorContext | một class thay thế, hoặc không gì cả |
| phương thức | phương thức đó | ClassMethodDecoratorContext | một phương thức thay thế |
| getter / setter | getter hoặc setter đó | ClassGetterDecoratorContext / ClassSetterDecoratorContext | một getter hoặc setter thay thế |
| thuộc tính | undefined | ClassFieldDecoratorContext | một hàm biến đổi giá trị ban đầu |
thuộc tính accessor | { get, set } | ClassAccessorDecoratorContext | { get?, set?, init? } |
Mọi đối tượng context đều có kind, name và addInitializer. Context của thành viên class còn có static, private và một đối tượng access để đọc thành viên đó từ một instance. Decorator cũng dùng được cho thành viên static và #private.
Decorator factory
Để truyền tùy chọn, hãy viết một hàm trả về decorator và gọi nó ngay tại chỗ @. Đây là một decorator factory:
@retry(3) gọi retry trước, và hàm mà nó trả về mới là decorator thật sự. Nhiều decorator có thể chồng lên nhau: trong @a @b method(), b được áp dụng trước và a bao kết quả đó.
Class decorator và addInitializer
Class decorator nhận chính class đó. Nó có thể trả về một class con để thay thế, hoặc không trả về gì và chỉ ghi nhận class ở đâu đó. context.addInitializer đăng ký code chạy vào một thời điểm cụ thể: với class decorator, ngay sau khi class được định nghĩa xong; với method decorator, mỗi khi một instance được tạo.
Class decorator đặt phía trên class (hoặc sau export). Không có @bound, gọi loose() sẽ ném lỗi, vì this sẽ là undefined.
Decorator cho thuộc tính và accessor
Decorator cho thuộc tính không thấy và không chặn được các phép gán về sau: value của nó là undefined, và thứ duy nhất nó có thể trả về là một hàm biến đổi giá trị ban đầu của thuộc tính. Để chặn việc đọc và ghi, hãy khai báo thuộc tính với từ khóa accessor, biến nó thành một cặp getter và setter dựa trên một vùng lưu trữ private, rồi trang trí nó:
accessor thuộc cùng đề xuất đó. Nó sinh ra getter và setter thật trên một thuộc tính #private, đó là lý do p.price = -5 đi qua set của decorator.
Decorator chuẩn và experimentalDecorators kiểu cũ
Trước TypeScript 5.0, decorator duy nhất mà TypeScript có là một phiên bản sớm của đề xuất, được bật bằng experimentalDecorators. Cờ đó vẫn còn, và nó chuyển trình biên dịch sang mô hình cũ, với chữ ký và ngữ nghĩa khác:
| Chuẩn (không cần cờ) | Kiểu cũ (experimentalDecorators) | |
|---|---|---|
| Chữ ký | (value, context) | (target, propertyKey, descriptor) |
| Cách thay đổi một phương thức | trả về một hàm mới | sửa trực tiếp descriptor.value |
| Parameter decorator | không hỗ trợ (TS1206) | có hỗ trợ |
emitDecoratorMetadata | không hỗ trợ | có hỗ trợ (thông tin kiểu lúc chạy qua reflect-metadata) |
Decorator cho accessor ({ get, set, init }), addInitializer | có | không |
| Dựa trên | đề xuất TC39 | một bản nháp cũ hơn của đề xuất đó |
Decorator viết cho mô hình này không qua được bước kiểm tra kiểu ở mô hình kia. Đây là một decorator kiểu cũ trong dự án không bật cờ:
index.ts(11,5): error TS1241: Unable to resolve signature of method decorator when called as an expression.
The runtime will invoke the decorator with 2 arguments, but the decorator expects 3.
Cách sửa là viết lại nó theo dạng chuẩn (value, context), như ví dụ đầu tiên trên trang này, hoặc bật mô hình cũ cho toàn bộ dự án:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Angular, NestJS và TypeORM vẫn đặt experimentalDecorators trong các cấu hình mà công cụ của chúng sinh ra và tài liệu của chúng yêu cầu. NestJS và TypeORM còn cần emitDecoratorMetadata, vì chúng đọc kiểu lúc chạy: NestJS để inject tham số của hàm khởi tạo, TypeORM để ánh xạ thuộc tính sang cột:
// Legacy model: needs experimentalDecorators (and emitDecoratorMetadata for DI).
@Injectable()
class UsersService {
constructor(@Inject(DB) private db: Database) {}
}
Nếu bạn dùng một trong các framework đó, hãy viết decorator theo kiểu cũ và làm theo tài liệu của framework. Với code mới không dùng framework nào như vậy, hãy dùng decorator chuẩn.
Khi nào nên dùng decorator
Decorator phù hợp với những hành vi xuyên suốt mà nếu không có nó sẽ phải lặp lại ở nhiều phương thức: ghi log, đo thời gian, cache, thử lại, kiểm tra quyền truy cập, kiểm tra dữ liệu, và đăng ký class với một container hoặc router. Chúng cũng che giấu luồng điều khiển, vì người đọc phải tra xem @retry làm gì rồi mới biết phương thức làm gì. Với một hai chỗ dùng, một hàm bậc cao bình thường (const fetchData = retry(3, rawFetch)) đơn giản hơn và dùng được cả bên ngoài class.
Câu hỏi thường gặp
Decorator trong TypeScript là gì?
Decorator là một hàm được gắn vào class hoặc thành viên của class bằng cú pháp @name. Nó nhận thứ được trang trí cùng một đối tượng context, và có thể trả về một thứ thay thế: một phương thức được bao bọc, một class mới, hoặc một hàm biến đổi giá trị ban đầu của một thuộc tính. Các cách dùng phổ biến là ghi log, kiểm tra dữ liệu, cache và đăng ký class.
Có cần experimentalDecorators để dùng decorator trong TypeScript không?
Không. Từ TypeScript 5.0, decorator chuẩn (TC39) hoạt động mà không cần cờ nào. experimentalDecorators chuyển trình biên dịch sang mô hình decorator cũ (legacy), nền tảng của các framework như Angular và NestJS. Hai mô hình dùng chữ ký hàm khác nhau và không thay thế cho nhau được.
Decorator chuẩn và experimentalDecorators khác nhau thế nào?
Decorator chuẩn nhận (value, context) và trả về một thứ thay thế. Decorator kiểu cũ nhận (target, propertyKey, descriptor) và sửa trực tiếp property descriptor. Chỉ mô hình cũ hỗ trợ parameter decorator và emitDecoratorMetadata; chỉ mô hình chuẩn có decorator cho accessor trả về { get, set, init } và context.addInitializer.
TypeScript có hỗ trợ parameter decorator không?
Chỉ khi bật experimentalDecorators. Ở chế độ chuẩn, decorator đặt trên tham số là lỗi TS1206: Decorators are not valid here, vì đề xuất TC39 không có parameter decorator. Vì vậy các framework dependency injection trang trí tham số của hàm khởi tạo cần đến cờ kiểu cũ.
Nhiều decorator được áp dụng theo thứ tự nào?
Biểu thức decorator được tính từ trên xuống dưới, nhưng được áp dụng từ dưới lên trên: trong @a @b method(), b bao phương thức trước và a bao kết quả đó. Vì vậy a chạy ở lớp ngoài cùng khi phương thức được gọi.