Advanced TypeScript Patterns for Clean Architecture
Nâng tầm code của bạn với các kỹ thuật TypeScript nâng cao: Discriminated Unions, Opaque Types và Dependency Injection. Viết code an toàn hơn, dễ bảo trì hơn.
Bạn đã làm quen với các cơ bản của TypeScript, nhưng làm thế nào để tận dụng sức mạnh của nó cho các hệ thống lớn và phức tạp? Dưới đây là 3 patterns nâng cao giúp code của bạn "sạch" và an toàn hơn.
1. Discriminated Unions (Tagged Unions)
Thay vì dùng any hoặc các interface lỏng lẻo, hãy dùng Discriminated Unions để mô hình hóa các trạng thái của ứng dụng một cách chính xác.
1typeLoadingState={ status:'loading'};2typeSuccessState={ status:'success'; data: User[]};3typeErrorState={ status:'error'; error: Error };45typeRequestState= LoadingState | SuccessState | ErrorState;67functionhandleState(state: RequestState){8switch(state.status){9case'loading':10return'Loading...';11case'success':12return`Loaded ${state.data.length} users`;13case'error':14return`Error: ${state.error.message}`;15// TypeScript sẽ báo lỗi nếu bạn quên handle một case nào đó (exhaustiveness checking)16}17}
Tại sao nó tốt?
Loại bỏ hoàn toàn việc check null hoặc undefined không cần thiết.
Tự động gợi ý code (intellisense) chính xác cho từng case.
2. Opaque Types (Branded Types)
Bạn có bao giờ nhầm lẫn giữa UserId và PostId vì cả hai đều là string? Opaque Types giúp bạn "đánh dấu" các kiểu dữ liệu nguyên thủy để tránh nhầm lẫn.
1typeUserId=string&{ __brand:"UserId"};2typePostId=string&{ __brand:"PostId"};34functiongetPost(id: PostId){/* ... */}56const userId ="user_123"as UserId;7const postId ="post_456"as PostId;89getPost(postId);// OK10getPost(userId);// Error: Argument of type 'UserId' is not assignable to parameter of type 'PostId'.
Tại sao nó tốt?
Type safety tuyệt đối cho các ID hoặc các giá trị primitive có ý nghĩa domain khác nhau.
3. Dependency Injection với Interface
Để code dễ test và lỏng lẻo (loose coupling), hãy luôn phụ thuộc vào Interface, không phải Class cụ thể.
1interfaceILogger{2log(message:string):void;3}45classConsoleLoggerimplementsILogger{6log(message:string){console.log(message);}7}89classUserService{10constructor(private logger: ILogger){}1112createUser(name:string){13this.logger.log(`Created user ${name}`);14}15}1617// Khi test, bạn có thể dễ dàng mock logger18const mockLogger ={ log: jest.fn()};19const service =newUserService(mockLogger);
Tại sao nó tốt?
Dễ dàng thay thế implementation (ví dụ: chuyển từ ConsoleLogger sang FileLogger mà không sửa code UserService).
Unit test dễ dàng hơn bao giờ hết.
Kết luận
TypeScript không chỉ là về việc thêm type cho biến. Nó là công cụ mạnh mẽ để thiết kế hệ thống. Hãy thử áp dụng các pattern này vào dự án tiếp theo của bạn nhé!