Sử Dụng TypeScript Trong Dự Án Web Lớn: Hướng Dẫn Thực Chiến

5 tháng trước · 9 phút đọc
Tại sao dự án lớn không thể thiếu TypeScript
Mình từng join một dự án React đã chạy 2 năm. Codebase hơn 80,000 dòng JavaScript thuần. Mỗi lần sửa một function, mình phải đọc ít nhất 5-7 file khác để hiểu data đang có hình dạng gì. Không có IDE nào gợi ý được. Không có gì báo lỗi trước khi chạy.
Đó là lý do TypeScript tồn tại.
Không có type = mò mẫm trong bóng tối khi codebase lớn dần
TypeScript không phải "JavaScript khó hơn". Nó là lớp bảo vệ giúp bạn phát hiện lỗi tại lúc viết code, thay vì lúc người dùng gặp bug trên production. Với dự án nhỏ, JS thuần vẫn ổn. Nhưng khi team có 5+ người, codebase vượt 10,000 dòng, TypeScript bắt đầu trả lời rất nhiều câu hỏi mà JS không thể.
Nhiều nghiên cứu cho thấy phần lớn dự án JavaScript lớn gặp lỗi runtime có thể phát hiện được ở compile time nếu dùng TypeScript - con số này lý giải tại sao hầu hết các tech stack hiện đại đều mặc định dùng TS.
Vấn đề thực sự không phải là "học TypeScript". Mà là học cách dùng TypeScript đúng cách trong môi trường team.
Cấu hình tsconfig đúng cho dự án lớn
tsconfig.json là nơi nhiều người setup cho có rồi thôi. Với dự án lớn, cấu hình sai ở đây sẽ khiến TypeScript không bảo vệ được gì đáng kể.
Mình recommend cấu hình tối thiểu sau:
Bật strict: true là bước đầu tiên - các options còn lại ngăn nhiều class bug phổ biến
strict: true không phải một option - nó bật 8 options cùng lúc, trong đó quan trọng nhất là strictNullChecks. Không có cái này, TypeScript gần như vô dụng.
noUncheckedIndexedAccess là option ít người biết nhưng cực kỳ hữu ích. Khi bạn dùng arr[0], TypeScript sẽ báo type là T | undefined thay vì T. Nghe phiền nhưng nó ngăn rất nhiều lỗi "Cannot read property of undefined".
paths giúp bạn dùng absolute imports thay vì ../../components/Button. Codebase 50 file thì không cần. Codebase 500 file thì bắt buộc.
Nếu bạn đang migrate một dự án JS cũ sang TS, đừng bật strict: true ngay. Bắt đầu với strict: false, từng bước bật từng option một. Một số team mình biết mất 3-6 tháng để migrate đúng cách - và đó là đầu tư xứng đáng.
Tải file tsconfig mẫu được tối ưu cho dự án lớn tại đây: Tải tsconfig mẫu cho dự án lớn
Type-safe API calls: tránh lỗi "surprise" từ backend
Một trong những nguồn bug phổ biến nhất trong frontend là dữ liệu từ API trả về khác với những gì frontend expect. Backend đổi tên field, thêm/bớt property - và frontend chỉ biết khi người dùng thấy màn hình trắng.
Cách fix? Define type cho mọi API response.
Generic wrapper + interface = IDE gợi ý đúng, lỗi type xuất hiện trước khi chạy
Nhưng đây là điểm mà nhiều người dừng lại và bỏ qua phần quan trọng hơn: runtime validation. TypeScript chỉ kiểm tra lúc compile. Khi code chạy trên browser, dữ liệu từ API vẫn là JavaScript thuần. Nếu backend trả về data sai shape, TypeScript không bắt được.
Giải pháp là dùng thư viện như Zod để validate schema tại runtime:
Zod đặc biệt hữu ích khi làm với dự án có nhiều team, nơi contract giữa frontend và backend hay thay đổi mà không có thông báo kịp thời.
Tổ chức types trong codebase lớn
Khi dự án lớn dần, câu hỏi không còn là "viết type thế nào" mà là "đặt type ở đâu".
Mình thấy 2 pattern phổ biến - và cả 2 đều có trade-off:
Pattern 1: Centralized types - tất cả types đặt trong thư mục src/types/
src/
types/
user.ts # UserProfile, UserSettings, v.v.
product.ts # Product, ProductVariant
api.ts # ApiResponse<T>, PaginatedResponse<T>
index.ts # Re-export tất cả
Lợi ích: dễ tìm, tránh duplicate. Nhược điểm: file types trở thành "trash can" khi dự án lớn, khó biết type nào còn dùng hay không.
Pattern 2: Co-located types - types đặt cạnh code dùng nó
src/
features/
user/
UserProfile.tsx
UserProfile.types.ts # Types riêng cho feature này
userApi.ts
Co-location giúp xóa types không còn dùng dễ hơn - xóa feature thì xóa luôn types
Mình prefer pattern 2 cho dự án theo kiến trúc feature-based. Nhưng với shared types dùng ở nhiều nơi (như ApiResponse<T>), vẫn cần một src/types/shared.ts riêng.
Một nguyên tắc khác: tránh dùng any. Mỗi lần bạn viết any, TypeScript mất khả năng bảo vệ tại điểm đó và tất cả những nơi nhận giá trị từ đó. Nếu cần escape hatch, dùng unknown rồi narrow type thay vì any.
Lint rule @typescript-eslint/no-explicit-any sẽ giúp enforce điều này tự động trong CI/CD.
Utility types: viết ít, bảo trì nhiều
Đây là phần TypeScript mà nhiều người học được nhưng ít người dùng đủ. Utility types giúp bạn tái sử dụng và biến đổi types thay vì viết lại từ đầu.
Các utility types mình dùng nhiều nhất trong dự án thực tế:
Partial<T> và Required<T> - thay đổi optional/required
Pick<T, K> và Omit<T, K> - chọn hoặc bỏ fields
Utility types giống LEGO - ghép lại từ các mảnh có sẵn thay vì đúc mới từ đầu
ReturnType<T> - lấy type từ return value của function
Pattern cuối này đặc biệt hữu ích khi làm với React Query hay SWR - type của data tự suy ra từ fetcher function, không cần khai báo thêm.
Nếu bạn muốn đi sâu hơn vào JavaScript trước khi bắt đầu TypeScript, khóa Lập Trình JavaScript Nâng Cao cover kỹ các khái niệm nền tảng như closure, prototype, reference types - những thứ giúp bạn hiểu TypeScript ở mức sâu hơn.
Strict mode trong team: enforce bằng ESLint, không bằng lời nói
Viết TypeScript tốt không đủ nếu teammate commit code bỏ qua tất cả. Cần tooling để enforce.
Setup mình recommend cho dự án lớn:
1. ESLint với @typescript-eslint
Các rule quan trọng nhất:
2. Pre-commit hook với Husky
Không ai muốn bị chặn commit, nhưng cũng không ai muốn review PR với 50 TypeScript errors. Chạy type-check trước khi push:
Pre-commit hook chặn lỗi trước khi vào main branch - đau ngay, đỡ đau về sau
3. CI/CD pipeline
Husky chỉ chạy ở máy developer. Ai đó có thể bypass. CI/CD là lớp cuối:
Thực tế mình thấy: team nào setup CI/CD type-check từ đầu thì số bug production giảm rõ rệt trong 2-3 sprint đầu. Không phải vì người dùng TypeScript giỏi hơn - mà vì tooling buộc phải sửa lỗi trước khi merge.
Tải checklist đầy đủ để setup TypeScript cho dự án lớn: Tải checklist setup TypeScript
