Interface vs type alias: chọn cái nào khi mô hình hóa domain
interface và type trong TypeScript phần lớn làm được việc giống nhau — mô tả hình dạng object — nên câu hỏi "dùng cái nào" hay bị trả lời tùy hứng. Thực tế có khác biệt rõ về khả năng: interface hỗ trợ declaration merging và extends tự nhiên cho object, hợp với mô hình domain mở rộng được; type linh hoạt hơn cho union, intersection, mapped/conditional type, tuple — những thứ interface không làm được. Chọn đúng giúp domain model dễ mở rộng và lỗi kiểu dễ đọc; chọn sai dẫn tới type lồng phức tạp khó bảo trì.
Cơ chế hoạt động
interface định nghĩa hình dạng object và có thể mở rộng bằng extends; nhiều khai báo cùng tên tự gộp (declaration merging). type là một alias cho bất kỳ kiểu nào — object, union, tuple, hàm — và kết hợp bằng intersection (&):
interface User { id: string; name: string }
interface User { email: string } // merge: User giờ có cả email
type Status = 'active' | 'banned' // union — interface không làm được
type WithTimestamps<T> = T & { createdAt: Date; updatedAt: Date } // intersection + generic
type Point = [number, number] // tuple
interface Admin extends User và type Admin = User & { role: string } cho kết quả tương tự cho object. Khác biệt lộ ra khi cần union/tuple/mapped type (chỉ type làm được) hoặc khi muốn cho phép mở rộng từ bên ngoài qua merging (chỉ interface).
Vấn đề gặp trong production
Khi nào interface hợp hơn: mô hình domain object dự kiến được mở rộng, hoặc kiểu public của một thư viện mà người dùng có thể augment. extends cho thông báo lỗi rõ hơn và quan hệ kế thừa đọc tự nhiên:
interface Entity { id: string; createdAt: Date }
interface Order extends Entity { items: Item[]; total: number }
Khi nào type hợp hơn: union (đặc biệt discriminated union — công cụ mạnh nhất để mô hình hóa state), tuple, hàm, hoặc các kiểu phái sinh bằng mapped/conditional type:
type Result<T> =
| { status: 'ok'; data: T }
| { status: 'error'; error: AppError } // discriminated union — type checker thu hẹp theo 'status'
function handle(r: Result<User>) {
if (r.status === 'ok') r.data // TS biết có data ở nhánh này
else r.error // và error ở nhánh kia
}
Discriminated union là pattern then chốt cho mọi state máy có nhiều biến thể (loading/loaded/error, các loại sự kiện) — chỉ type diễn đạt gọn.
Failure mode: recursive/conditional type quá phức tạp. type cho phép viết type đệ quy và conditional mạnh tới mức tạo ra những kiểu không ai đọc nổi, compile chậm, và thông báo lỗi dài hàng chục dòng. Đây là cái bẫy "vì làm được nên làm": một type lồng sâu thông minh thường nên thay bằng một kiểu đơn giản hơn hoặc tách nhỏ. Độ phức tạp của type cũng là nợ kỹ thuật.
Cách debug và monitor
Khi thông báo lỗi kiểu dài và khó hiểu, thường là dấu hiệu type đã quá phức tạp — dùng tính năng "go to type definition" của editor và hover để xem kiểu được giải ra thế nào, hoặc gán vào một biến trung gian để compiler in kiểu thực tế. Compile chậm dần khi codebase lớn có thể do conditional/mapped type nặng; profiler của TS (--extendedDiagnostics, --generateTrace) chỉ ra type nào tốn thời gian kiểm tra. Quy ước nhóm rõ ràng (interface cho object domain, type cho union/utility) giảm tranh luận và giữ codebase nhất quán — sự nhất quán đáng giá hơn việc chọn "đúng tuyệt đối".
Tradeoff
interface cho declaration merging và quan hệ extends đọc tự nhiên, hợp domain object mở rộng được và API công khai; đổi lại không biểu diễn được union/tuple/mapped type. type linh hoạt cho mọi cấu trúc kiểu (union, intersection, tuple, conditional) — mạnh nhưng dễ bị lạm dụng thành type phức tạp khó bảo trì và làm chậm compile. Quy tắc thực tế phổ biến: dùng interface cho hình dạng object (nhất là domain model và kiểu public), dùng type cho union/tuple/utility và kiểu phái sinh; ưu tiên discriminated union để mô hình state; và giữ type đủ đơn giản để đọc được — thông minh quá là nợ.
Câu hỏi phỏng vấn
typevàinterfacekhác nhau ở đâu, và khi nào nên chọn cái nào?
Cả hai mô tả được hình dạng object và phần lớn thay thế cho nhau ở việc đó, nhưng khác về khả năng: interface hỗ trợ declaration merging (nhiều khai báo cùng tên tự gộp) và extends cho object đọc tự nhiên, hợp domain model mở rộng được và kiểu public mà người dùng có thể augment; type là alias cho bất kỳ kiểu nào nên làm được union, intersection, tuple, mapped/conditional type — những thứ interface không biểu diễn được, đặc biệt là discriminated union để mô hình hóa state nhiều biến thể. Quy tắc thực tế: interface cho hình dạng object domain/API, type cho union/tuple/utility và kiểu phái sinh. Điểm ăn điểm: cảnh báo type đệ quy/conditional quá phức tạp gây compile chậm và lỗi khó đọc — giữ type đơn giản, và sự nhất quán trong nhóm quan trọng hơn chọn "đúng tuyệt đối".
Hands-on
Mô hình hóa một domain thật (ví dụ hệ thống đơn hàng) bằng interface cho các entity (Entity, Order extends Entity, User) và bằng type cho các trạng thái xử lý dưới dạng discriminated union (Result<T> = ok | error, hoặc trạng thái đơn loading/paid/shipped/cancelled). Viết một hàm xử lý dựa trên discriminated union và xác nhận type checker tự thu hẹp kiểu theo trường phân biệt. Sau đó cố tình viết một utility type lồng sâu (mapped + conditional) tới mức lỗi kiểu trở nên khó đọc và compile chậm, rồi refactor nó thành kiểu đơn giản hơn — so sánh độ dài thông báo lỗi và thời gian compile trước/sau.
Top comments (0)