Menu

Decorator trong TypeScript: method, class, field, accessor

Decorator là các hàm bao bọc hoặc thay thế thành viên của class bằng cú pháp @. Tìm hiểu các decorator chuẩn mà TypeScript hỗ trợ không cần cờ nào (class, method, getter, field và accessor), decorator factory, addInitializer, và chúng khác gì experimentalDecorators kiểu cũ mà Angular và NestJS dùng.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

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ívalueKiểu contextTrả về
classchính class đóClassDecoratorContextmột class thay thế, hoặc không gì cả
phương thứcphương thức đóClassMethodDecoratorContextmột phương thức thay thế
getter / settergetter hoặc setter đóClassGetterDecoratorContext / ClassSetterDecoratorContextmột getter hoặc setter thay thế
thuộc tínhundefinedClassFieldDecoratorContextmộ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ứctrả về một hàm mớisửa trực tiếp descriptor.value
Parameter decoratorkhông hỗ trợ (TS1206)có hỗ trợ
emitDecoratorMetadatakhông hỗ trợcó hỗ trợ (thông tin kiểu lúc chạy qua reflect-metadata)
Decorator cho accessor ({ get, set, init }), addInitializercókhông
Dựa trênđề xuất TC39mộ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.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu