TypeScript decorators: Cách viết và dùng class decorators thực tế

TypeScript decorators là một tính năng mạnh mẽ cho phép chúng ta thêm các hành vi (behavior) vào lớp, phương thức hoặc thuộc tính một cách khai báo. Decorators giúp tách biệt logic ẩn (cross-cutting concerns) như ghi nhật ký, xác thực hay caching ra khỏi nhân thân hàm, giúp mã nguồn gọn hơn và dễ bảo trì hơn. Trong bài viết này, chúng ta sẽ cùng khám phá cách viết và dùng class decorators thực tế trong dự án.

Logo TypeScript ngôn ngữ lập trình
Sơ đồ UML mô tả design pattern decorator áp dụng trong TypeScript

Cấu trúc và cú pháp cơ bản của decorator

Một decorator là một hàm nhận vào đối tượng mà chúng ta muốn trang trí, sau đó trả về phiên bản mới (hoặc cùng một bản) của đối tượng đó. Cú pháp sử dụng ký tự @ và được đặt ngay trước khai báo lớp hoặc thành phần cần trang trí. Để bật tính năng này, chúng ta cần bật experimentalDecorators trong file tsconfig.json:

{
  "compilerOptions": {
    "target": "es2020",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Chú ý: emitDecoratorMetadata giúp TypeScript tự động đưa thông tin kiểu (type metadata) vào thời gian chạy, hữu ích khi kết hợp với framework như NestJS.

Các loại decorator phổ biến

Tên decorator Áp dụng Mục đích
Class decorator Lớp Thay đổi hoặc mở rộng hành vi lớp
Method decorator Phương thức Ghi đè hoặc bổ sung logic trước/sau hàm
Property decorator Thuộc tính Theo dõi hoặc kiểm chứng giá trị
Accessor decorator get/set Tùy biến hành vi getter/setter
Parameter decorator Tham số hàm Đăng ký hoặc tiêm dependency

Ví dụ viết class decorators thực tế

Giả sử chúng ta muốn ghi lại thời gian thực thi của một phương thức. Thay vì sửa đổi code trong từng hàm, chúng ta tạo một method decorator tái sử dụng:

function Timer(label: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const start = performance.now();
      const result = original.apply(this, args);
      const end = performance.now();
      console.log(`[${label}] Thời gian chạy: ${end - start}ms`);
      return result;
    };
    return descriptor;
  };
}

class Service {
  @Timer('Tính tổng')
  sum(n: number): number {
    let total = 0;
    for (let i = 0; i <= n; i++) total += i;
    return total;
  }
}

const svc = new Service();
svc.sum(1000);

Bạn có thể thấy logic đo thời gian được tách biệt, và chúng ta chỉ cần gán @Timer(...) lên bất kỳ hàm nào. Đây chính là lợi ích lớn nhất của decorator.

Class decorator nâng cao: tự nhận dạng lớp

Class decorator nhận vào constructor của lớp. Chúng ta có thể dùng nó để tự động đăng ký (register) lớp vào một bộ nhớ toàn cục, rất hữu ích trong các framework dependency injection:

const registry: any[] = [];

function Register() {
  return function (target: any) {
    registry.push(target);
    return target;
  };
}

@Register()
class UserService {
  find(id: number) {
    return { id, name: 'Người dùng A' };
  }
}

console.log(registry.length); // 1

Lưu ý khi sử dụng decorators trong thực tế

Dù rất tiện lợi, decorators cũng có một số điểm cần lưu ý. Thứ nhất, decorator chỉ là cú pháp đường người (syntactic sugar), nên nếu một nhà phát triển chưa quen, mã nguồn sẽ khó đọc hơn. Hai, tránh viết decorator quá phức tạp vì sẽ khó gỡ lỗi và bảo trì. Ba, luôn kiểm tra kết quả thời gian chạy: một số decorator trả về void có thể ghi đè hành vi mặc định và gây lỗi khó tìm.

Ngoài ra, khi dự án tách thành nhiều module, hãy đưa các decorator tái sử dụng vào một file riêng (ví dụ decorators.ts) để toàn bộ dự án chung cấu trúc. Điều này giúp áp dụng chuẩn linting và chia sẻ qua các gói thư viện nội bộ.

Giao diện Visual Studio Code đang soạn thảo code TypeScript

Kết luận

Qua những ví dụ trên, chúng ta thấy TypeScript decorators không chỉ giúp viết mã sạch mà còn tăng tính tái sử dụng và khả năng bảo trì. Tuy nhiên, để khai thác tối đa, bạn nên kết hợp decorators với các framework hiện đại như NestJS, trong đó cách dùng được chuẩn hóa hoàn toàn. Khi áp dụng, luôn ưu tiên tính rõ ràng: mỗi decorator chỉ nên chịu trách nhiệm một nhiệm vụ duy nhất, tránh “bí mật” xảy ra bên trong hàm gốc.

Nguồn tham khảo: Tài liệu chính thức TypeScript (decorators), Wikipedia về TypeScript, Đề xuất decorator của TC39.

Các thư viện decorator phổ biến trong TypeScript

Nhiều nhà phát triển tận dụng các sẵn có để tránh phải tự viết decorator cho những nhu cầu phổ biến. Hai thư viện nổi bật nhất là core-decorators và autobind-decorator.

core-decorators cung cấp một bộ sưu tập decorator được chuẩn hoá, bao gồm các decorator như @autobind (tự động bind this cho phương thức), @readonly (đặt thuộc tính chỉ đọc), @deprecate (hiển thị cảnh báo khi hàm đã lỗi thời) và @memoize (lưu trữ kết quả của hàm dựa vào tham số). Ví dụ sử dụng:

import { autobind, readonly, deprecate } from 'core-decorators';

class Calculator {
  @autobind
  add(a: number, b: number): number {
    return a + b;
  }

  @readonly
  version = '1.0.0';

  @deprecate('Vui lòng sử dụng multiply_v2 thay vì multiply')
  multiply(a: number, b: number): number {
    return a * b;
  }
}

Decorator @autobind rất hữu ích trong các callback được truyền xuống component trong React, khi mà không cần viết lại .bind(this) mỗi khi truyền prop.

autobind-decorator focus hơn vào việc tự động bind hàm, thích hợp với các class service được gọi qua event listener.

Một decorator khác đáng nhắc đến là @measure từ decko, giúp đo thời gian chạy với khả năng cấu hình ngưỡng cảnh báo. Khi dự án có yêu cầu đo hiệu suất chi tiết, việc tích hợp decorator vào pipeline CI/CD để cảnh báo khi hàm chậm hơn một giây sẽ giúp phát hiện vấn đề sớm.

Nhờ các thư viện sẵn có, chúng ta chỉ cần tập trung vào logic nghiệp vụ, trong khi các khía cạnh cắt ngang (logging, validation, caching) được xử lý một cách nhất quán và giảm thiểu sai sót do con người.

Hướng dẫn tối ưu hiệu suất khi dùng decorator

Decorator hoạt động bằng cách tạo bọc (wrapper) quanh hàm hoặc lớp, vì vậy việc lạm dụng có thể gây thêm chi phí về bộ nhớ và thời gian thực thi. Dưới đây là một số mẹo:

  • Giữ decorator nhẹ: Tránh thực hiện các thao tác I/O hoặc tính toán phức tạp trong decorator, trừ khi thực sự cần thiết.
  • Kiểm tra phạm vi áp dụng: Chỉ dùng decorator trên những thành phần thực sự cần thiết thay vì cho toàn bộ lớp.
  • Sử dụng memoization cẩn thận: Decorator @memoize có thể tăng tiêu thụ bộ nhớ nếu bộ nhớ đệm không được xóa định kỳ.
  • Lưu ý khi kết hợp với TypeScript compile: Khi bật emitDecoratorMetadata, biên dịch sẽ sinh ra thêm metadata, làm tăng kích thước file đầu ra. Đảm bảo rằng không bật tùy chọn này khi không có framework nào cần dùng.

Đo lường thực tế qua công cụ như Chrome DevTools Performance panel hoặc benchmark benchmark.js sẽ cho bạn cái nhìn cụ thể về chi phí thêm.

Kết luận mở rộng

Decorator là một trong những tính năng khiến TypeScript trở nên linh hoạt hơn trong việc mô tả các khía cạnh cắt ngang (AOP) mà không cần cấu hình phức tạp. Khi kết hợp với hệ sinh thái npm phong phú, chúng ta có thể xây dựng các framework nội bộ hoặc mở rộng các công cụ hiện có một cách minh bạch và bảo trì được. Hãy luôn nhớ rằng mục tiêu của decorator không phải là làm code “bí ẩn”, mà là tách biệt trách nhiệm một cách rõ ràng, để mỗi nhà phát triển có thể tập trung vào phần logic chính mà không lo lắng về chi phí phụ.

Nếu bạn đang bắt đầu một dự án TypeScript mới, hãy thử áp dụng decorator cho một nhu cầu nhỏ như logging hoặc validation. Khi thấy lợi ích, mở rộng dần sang các trường hợp phức tạp hơn. Thực hành sẽ giúp bạn nắm vững cách thiết kế decorator một cách đúng đắn và hiệu quả.

Nguồn tham khảo bổ sung: core-decorators GitHub, autobind-decorator GitHub, decko GitHub.

Tôi là một lập trình viên IOS. Code chính là IOS nhưng thỉnnh thoảng vẫn đá sang Android hoặc web. Mặc dù không quá thông thạo nhưng tôi sẽ chia sẻ những kiến thức mà mình đã tìm hiểu, áp dụng qua.

Bài viết liên quan

Event Sourcing và CQRS: kiến trúc sự kiện cho app thương mại

Event Sourcing và CQRS: kiến trúc sự kiện cho app thương mại Event Sourcing (ES) là một mô hình lưu trữ dữ liệu trong đó trạng thái của một thực…

Xem thêm

Kubernetes pods resource management: requests vs limits

Kubernetes pods resource management: requests vs limits Trong môi trường container orchestration, việc quản lý tài nguyên một cách hiệu quả không chỉ giúp tối ưu chi phí mà còn…

Xem thêm
docker_twistlock

Docker multi-stage builds: Cách thu nhỏ image xuống 90%

Docker multi-stage builds: Cách thu nhỏ image xuống 90% Một trong những thách thức lớn nhất khi dùng Docker là image size quá lớn. Một image Python đơn giản có…

Xem thêm
0 0 đánh giá
Article Rating
Theo dõi
Thông báo của
guest
0 Comments
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất