Bài 3 · Vận dụng · 22 phút
Interface & structural typing
Biên soạn bởi Nguyễn Anh Tuấn
Mô tả hình dạng dữ liệu bằng interface và type alias (optional, readonly, index signature); vì sao TS tương thích theo HÌNH DẠNG chứ không theo tên (structural / duck typing, khác nominal của Java/C#); excess property check gây bất ngờ. Bài under the hood của hệ thống kiểu.
Ở bài trước ta viết kiểu object ngay tại chỗ: { ten: string; tuoi: number }.
Khi hình dạng lớn dần và lặp lại nhiều nơi, hãy đặt tên cho nó bằng interface hoặc type:
dat-ten-hinh-dang.ts
interface Meo {
ten: string;
tuoi: number;
mau?: string; // optional: co cung duoc, thieu cung duoc
readonly id: number; // readonly: gan mot lan, khong sua lai
}
type Diem = { x: number; y: number }; // type alias cho object
interface BangDiem {
[mon: string]: number; // index signature: khoa string bat ky, gia tri number
} - ▸interface và type alias đều mô tả hình dạng một object để tái dùng.
- ▸Hậu tố ? làm field optional; readonly chặn gán lại (lúc biên dịch).
- ▸Index signature [key: string]: T mô tả object có khoá động, không cố định.
Đây là điểm "under the hood" hay làm người từ Java/C# bất ngờ. TypeScript so khớp kiểu theo hình dạng (structural typing), không theo tên khai báo. Một object chỉ cần đúng hình dạng là dùng được như Meo, dù chưa từng được khai báo là Meo:
structural.ts
interface Meo {
ten: string;
tuoi: number;
}
function gioiThieu(m: Meo): string {
return m.ten + ", " + m.tuoi + " tuoi";
}
const obj = { ten: "Bo", tuoi: 2, mau: "den" }; // co them mau
console.log(gioiThieu(obj)); // OK! obj du ten+tuoi nen khop hinh dang Kết quả khi chạy
Bo, 2 tuoi
Nghịch thử: bật tắt field xem object còn "khớp hình dạng" Meo hay không.
Mục tiêu: gán object của mèo con cho một biến kiểu Meo = { ten: string; tuoi: number }. Bật/tắt field xem có lọt không:
Object của mèo con: { ten: string; tuoi: number }
✅ Gán được - hình dạng khớp (field thừa không sao).
Khác với Java/C# (nominal typing)
Structural typing nói "thừa field thì bỏ qua" - đúng, nhưng có một ngoại lệ cố ý. Khi bạn gán thẳng một object literal có field thừa, TypeScript siết lại và báo lỗi. Gọi là excess property check:
Gán literal thừa field → lỗi
interface Meo { ten: string; tuoi: number; }
const m: Meo = { ten: "Bo", tuoi: 2, mau: "den" }; tsc báo
error TS2353: Object literal may only specify known properties, and 'mau' does not exist in type 'Meo'. Vì sao cùng object đó qua biến (Bước 2) thì lọt, mà gán thẳng lại lỗi? Vì khi bạn gõ literal ngay tại chỗ, một field lạ gần như chắc chắn là gõ nhầm - nên TS bắt giúp. Đây là một tính năng, không phải mâu thuẫn.
- ▸Object literal gán thẳng bị kiểm field thừa (excess property check) để bắt lỗi gõ tên.
- ▸Cùng object qua một biến trung gian thì theo quy tắc structural thường (thừa field bỏ qua).
- ▸Đây là lưới an toàn cho lỗi gõ nhầm tên thuộc tính, không phải mâu thuẫn.
Cả hai mô tả hình dạng được, nên với object thường thì chọn cái nào cũng ổn. Hai khác biệt đáng nhớ: interface có thể extends và gộp khai báo; type làm được nhiều hơn với union, intersection và kiểu nâng cao:
extends vs intersection
interface DongVat { ten: string; }
interface Meo extends DongVat { keu: string; } // interface: extends
type DongVat2 = { ten: string };
type Meo2 = DongVat2 & { keu: string }; // type: giao kieu (&) - ▸Object thường: interface hay type đều được, chọn theo thói quen đội nhóm.
- ▸Cần extends/gộp khai báo, mô tả class: nghiêng về interface.
- ▸Cần union, intersection, hay kiểu nâng cao sau này: dùng type alias.
Bạn vừa biết cách đặt tên cho hình dạng dữ liệu, và nắm điểm cốt lõi: TypeScript khớp kiểu theo hình dạng, không theo tên. Một nơi structural typing lộ rõ nhất là khi một class hứa sẽ "có hình dạng" của một interface.
Bài tiếp theo: Class trong TypeScript
implements một interface - structural typing ở dạng có tên.Câu hỏi thường gặp
Với hình dạng object, hai cái gần như thay thế được nhau. Khác biệt: interface có thể được extends và "gộp khai báo" (declaration merging), hợp khi mô tả object và class. type alias thì linh hoạt hơn cho union, intersection, primitive, tuple và các kiểu nâng cao sau này. Quy tắc gọn: object dùng cái nào cũng được; cần union/giao kiểu thì dùng type.
Phần lớn là tiện: bạn không phải khai báo "object này là Meo" một cách rườm rà, chỉ cần đúng hình dạng. Rủi ro là hai thứ tình cờ cùng hình dạng bị coi là một. Trong thực tế điều đó hiếm khi gây hại, và TypeScript có excess property check (Bước 3) cùng kỹ thuật "branded type" (nâng cao) để siết khi cần.
Đó là excess property check: TypeScript siết riêng với object literal gán thẳng, vì lúc đó field thừa gần như chắc chắn là gõ nhầm. Khi đi qua một biến trung gian, TS dùng quy tắc structural thông thường (thừa field thì bỏ qua). Mẹo: gán literal đúng kiểu sẽ giúp bắt lỗi gõ tên sớm.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Structural typing trong TypeScript nghĩa là gì?
- 1
Đặt tên hình dạng
Lấy một object trong code của mèo con (hoặc tự nghĩ một cái, vd một cuốn sách). Viết một interface mô tả nó, có ít nhất một field optional và một field readonly.
✅ Hoàn thành khi: Một interface biên dịch sạch, dùng được làm kiểu cho biến, có đúng một field optional (?) và một field readonly.
- 2
Khớp theo hình dạng
Viết một hàm nhận interface ở bài trên. Tạo một object có ĐỦ field cần thiết và THỪA thêm một field lạ, gán vào một biến rồi truyền vào hàm.
✅ Hoàn thành khi: Hàm chạy bình thường dù object thừa field, chứng minh TS khớp theo hình dạng (qua biến trung gian).
- 3
Chạm phải excess property check
Lần này truyền THẲNG một object literal có field thừa vào hàm (không qua biến). Xem tsc báo lỗi gì.
✅ Hoàn thành khi: Ghi lại thông báo lỗi excess property (TS2353) và một câu giải thích vì sao literal bị siết còn biến thì không.
- 4
Chơi với widget hình dạng
Mở widget ở Bước 2. Tìm tổ hợp bật/tắt nhỏ nhất khiến object KHÔNG gán được cho Meo, và một tổ hợp có field thừa mà VẪN gán được.
✅ Hoàn thành khi: Hai tổ hợp cụ thể: một cái lỗi (kèm lý do thiếu/sai kiểu), một cái thừa field nhưng vẫn hợp lệ.
- 5
readonly không phải khoá runtime
Khai báo một interface có field readonly, tạo object, thử gán lại field đó. Xem tsc chặn. Rồi biên dịch ra .js và thử sửa trong JS.
✅ Hoàn thành khi: Bằng chứng: tsc báo lỗi readonly (TS2540), nhưng file .js sinh ra sửa được - vì kiểu đã bị xoá.
- 6
interface hay type?
Nêu hai tình huống: một cái nên dùng interface, một cái buộc phải dùng type alias (vd cần một union).
✅ Hoàn thành khi: Hai ví dụ kèm lý do: vì sao tình huống này hợp interface, vì sao tình huống kia cần type.