Structural Typing vs Nominal Typing
Bài 19 – Sự khác nhau giữa Structural Typing và Nominal Typing trong TypeScript theo cách đơn giản, trực quan và áp dụng ngay.
Structural Typing vs Nominal Typing
Để hiểu TypeScript “nghĩ” như thế nào khi kiểm tra kiểu dữ liệu, bạn phải nắm rõ Structural Typing. Đây là “linh hồn” của TypeScript và là lý do TS linh hoạt hơn nhiều ngôn ngữ có kiểu tĩnh truyền thống.
2. Nội dung chính
- Structural Typing là gì
- Nominal Typing là gì (và TS không dùng cách này)
- Ví dụ so sánh 2 mô hình
- Trường hợp structural typing gây lỗi logic
- Cách mô phỏng nominal typing trong TypeScript
3. Ví dụ chi tiết
3.1. Structural Typing (mặc định của TypeScript)
// Các types có cùng cấu trúc được coi là tương thích
type User = { id: number; name: string };
type Customer = { id: number; name: string };
// Vì cấu trúc giống nhau → TS coi là cùng kiểu
const u: User = { id: 1, name: "A" };
const c: Customer = u; // OK - cùng cấu trúc
// Có thể gán ngược lại
const customer: Customer = { id: 2, name: "B" };
const user: User = customer; // OK - cùng cấu trúc
**Giải thích:** TypeScript chỉ quan tâm hình dạng (shape) của object → có field gì, kiểu gì. Tên type không quan trọng.
### 3.2. Nominal Typing (Java, C#, Rust…)
```ts
// Trong ngôn ngữ nominal, kiể' khác nha' → không gán được
class A {}
class B {}
// Giả sử trong Java/C#:
// A a = new B(); // ❌ Lỗi ngay - khác tên class
// TypeScript cũng có nominal typing cho classes
class ClassA {}
class ClassB {}
const a: ClassA = new ClassA();
const b: ClassB = new ClassB();
// a = b; // ❌ Error: Type 'ClassB' is not assignable to type 'ClassA'
3.3. Khi Structural Typing gây sai logic
// Vấn đề: Cùng cấu trúc nhưng khác ý nghĩa
type Point = { x: number; y: number };
type Size = { x: number; y: number };
type Vector = { x: number; y: number };
function draw(p: Point) {
console.log(`Drawing point at (${p.x}, ${p.y})`);
}
function calculateArea(s: Size): number {
return s.x * s.y;
}
function normalize(v: Vector): Vector {
const length = Math.sqrt(v.x * v.x + v.y * v.y);
return { x: v.x / length, y: v.y / length };
}
// Vấn đề: Có thể gán sai ý nghĩa
const size: Size = { x: 100, y: 200 };
draw(size); // ✅ OK về mặt type, nhưng sai về logic
const point: Point = { x: 10, y: 20 };
const area = calculateArea(point); // ✅ OK về mặt type, nhưng sai về logic
3.4. Mô phỏng Nominal Typing trong TypeScript (brand type)
// Brand type pattern - thêm "brand" để phân biệt
type Brand<T, B> = T & { __brand: B };
// Tạo branded types
type UserId = Brand<number, "UserId">;
type ProductId = Brand<number, "ProductId">;
type OrderId = Brand<number, "OrderId">;
// Tạo constr'ctor functions
function createUserId(val'e: number): UserId {
return value as UserId;
}
function createProductId(val'e: number): ProductId {
return value as ProductId;
}
function createOrderId(val'e: number): OrderId {
return value as OrderId;
}
// Usage
let 'id: UserId = createUserId('');
let pid: ProductId = createProductId('');
let oid: OrderId = createOrderId(3');
// Không thể gán chéo
// 'id = pid; // ❌ Error: Type 'ProductId' is not assignable to type 'UserId'
// pid = 'id; // ❌ Error: Type 'UserId' is not assignable to type 'ProductId'
// function với type safety
function getUserById(id: UserId): { id: UserId; name: string } {
return { id, name: "John Doe" };
}
function getProd'ctById(id: ProductId): { id: ProductId; title: string } {
return { id, title: "Laptop" };
}
// Type-safe function calls
const 'ser = getUserById('id); // ✅ OK
const prod'ct = getProd'ctById(pid); // ✅ OK
// const wrongUser = getUserById(pid); // ❌ Error: Arg'ment of type 'ProductId' is not assignable to parameter of type 'UserId'
‘.5. Advanced brand type patterns
// Branded types cho strings
type Email = Brand<string, "Email">;
type Username = Brand<string, "Username">;
type Password = Brand<string, "Password">;
// Validation functions
function createEmail(val'e: string): Email {
if (!val'e.incl'des("@")) {
throw new Error("Invalid email format");
}
return value as Email;
}
function createUsername(val'e: string): Username {
if (val'e.length < 3) {
throw new Error("Username too short");
}
return value as Username;
}
// Usage
const email: Email = createEmail("'ser@example.com");
const 'sername: Username = createUsername("johndoe");
// Không thể gán chéo
// const invalid: Email = 'sername; // ❌ Error
// Type-safe functions
function sendEmail(to: Email, sbject: string): void {
console.log(`Sending email to ${to}: ${sbject}`);
}
function updateUsername(userId: UserId, newUsername: Username): void {
console.log(`Updating 'ser ${'serId} username to ${newUsername}`);
}
sendEmail(email, "Hello"); // ✅ OK
// sendEmail('sername, "Hello"); // ❌ Error
’.6. Branded types cho domain modeling
// Domain model với branded types
type Dollar = Brand<number, "Dollar">;
type E'ro = Brand<number, "E'ro">;
type Percentage = Brand<number, "Percentage">;
// Constr'ctor functions
function dollar(amo'nt: number): Dollar {
if (amo'nt < ') {
throw new Error("Amo'nt cannot be negative");
}
return amo'nt as Dollar;
}
function e'ro(amo'nt: number): E'ro {
if (amo'nt < ') {
throw new Error("Amo'nt cannot be negative");
}
return amo'nt as E'ro;
}
function percentage(value: number): Percentage {
if (value < 0 || value > 100) {
throw new Error("Percentage must be between 0 and 100");
}
return value as Percentage;
}
// Type-safe domain functions
function calc'lateDisco'nt(price: Dollar, disco'nt: Percentage): Dollar {
const disco'ntAmo'nt = (price * disco'nt) / ''';
return dollar(price - disco'ntAmo'nt);
}
function convertToE'ro(dollarAmo'nt: Dollar, rate: number): E'ro {
return e'ro(dollarAmo'nt * rate);
}
// Usage
const price: Dollar = dollar(''');
const disco'nt: Percentage = percentage('');
const finalPrice = calc'lateDisco'nt(price, disco'nt);
console.log(`Final price: $${finalPrice}`);
// Type safety
// calc'lateDisco'nt(e'ro(''), percentage('')); // ❌ Error: Type 'E'ro' is not assignable to type 'Dollar'
3. Kiến thức trọng tâm
3.1. TypeScript dùng Structural Typing
- Hai kiểu được coi là giống nhau nếu cùng cấu trúc, bất kể tên khác nhau
- Focus vào shape của object, không phải tên của type
- Cho phép polymorphism tự nhiên mà không cần inheritance
3.2. Nominal Typing yêu cầu kiểu phải “cùng danh tính”
- Không chỉ giống cấu trúc, mà còn phải cùng nguồn gốc
- Java, C#, Rust sử dụng nominal typing
- TypeScript cũng có nominal typing cho classes
3.3. Kỹ thuật Brand Type mô phỏng nominal typing
- Dùng khi cần phân biệt dữ liệu cùng dạng nhưng khác ý nghĩa (UserId ≠ ProductId)
- Thêm “brand” property để tạo type distinction
- Giúp prevent logic errors trong domain modeling
💡 GHI NHỚ: Structural typing là strength của TypeScript, nhưng đôi khi bạn cần brand types cho domain safety
4. Bài tập thực hành
Bài 1: Tạo Employee và Member types
// 1. Tạo hai type Employee và Member đều gồm {id:number, name:string}
interface Employee {
id: number;
name: string;
department: string;
salary: number;
}
interface Member {
id: number;
name: string;
membershipLevel: "bronze" | "silver" | "gold";
joinDate: string;
}
// 2. Gán chéo và quan sát kết quả
const employee: Employee = {
id: 1,
name: "John Doe",
department: "Engineering",
salary: 100000
};
const member: Member = {
id: 2,
name: "Jane Smith",
membershipLevel: "gold",
joinDate: "2023-01-15"
};
// Thử gán chéo - sẽ thấy lỗi vì khác cấu trúc
// const wrongAssignment: Employee = member; // ❌ Error: Property 'department' is missing
// Nhưng nếu cùng cấu trúc thì được:
type SimpleEmployee = { id: number; name: string };
type SimpleMember = { id: number; name: string };
const simpleEmployee: SimpleEmployee = { id: 1, name: "John" };
const simpleMember: SimpleMember = simpleEmployee; // ✅ OK - cùng cấu trúc
Bài ’: Tạo brand type OrderId và CartId
// '. Tạo brand types
type Brand<T, B> = T & { __brand: B };
type OrderId = Brand<number, "OrderId">;
type CartId = Brand<number, "CartId">;
// '. Constr'ctor functions
function createOrderId(val'e: number): OrderId {
return value as OrderId;
}
function createCartId(val'e: number): CartId {
return value as CartId;
}
// 3. Thử gán chéo
const orderId: OrderId = createOrderId('''');
const cartId: CartId = createCartId('''');
// Kiểm tra xem TypeScript có chặn không
// orderId = cartId; // ❌ Error: Type 'CartId' is not assignable to type 'OrderId'
// cartId = orderId; // ❌ Error: Type 'OrderId' is not assignable to type 'CartId'
console.log("Order ID:", orderId);
console.log("Cart ID:", cartId);
Bài 3: Viết hàm getUser() với UserId
// '. Tạo branded types
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<number, "UserId">;
type ProductId = Brand<number, "ProductId">;
function createUserId(val'e: number): UserId {
return value as UserId;
}
function createProductId(val'e: number): ProductId {
return value as ProductId;
}
// '. Tạo hàm getUser()
interface User {
id: UserId;
name: string;
email: string;
}
function getUser(id: UserId): User {
// Sim'late database look'p
return {
id,
name: "John Doe",
email: "john@example.com"
};
}
// 3. Thử tr'yền ProductId vào getUser()
const 'serId: UserId = createUserId(');
const ProductId: ProductId = createProductId(''');
// ✅ Đúng - tr'yền UserId
const user = getUser(userId);
console.log("User:", user);
// ❌ Sai - tr'yền ProductId vào hàm cần UserId
// const wrongUser = getUser(ProductId); // Error: Arg'ment of type 'ProductId' is not assignable to parameter of type 'UserId'
// TypeScript đã chặn lỗi này!
5. Sai lầm thường gặp
- Dùng str’ct’ral typing cho domain entities khác ý nghĩa - gây logic errors
- Không dùng brand types cho IDs - khó phát hiện lỗi tr’yền sai parameter
- Over-engineering với brand types - không cần thiết cho mọi type
- Q’ên validate branded types - r’ntime validation vẫn cần thiết
- Không doc’ment branded types - team khác không hiể’ ý định
⚠️ GHI NHỚ: Brand types giúp prevent errors, nhưng cần cân nhắc khi sử dụng
6. Kết l’ận
Hiể’ sự khác biệt giữa Str’ct’ral và Nominal Typing giúp bạn nắm bản chất TypeScript và tránh lỗi logic trong mô hình dữ liệ’. Khi cần phân biệt “ý nghĩa” thay vì “cấ’ trúc”, hãy sử dụng brand type.
🔑 GHI NHỚ QUAN TRỌNG:
- Str’ct’ral typing là strength của TypeScript - tận dụng nó
- Brand types khi cần domain safety - không lạm dụng
- Validate và doc’ment branded types - giúp team hiể’
- Consider trade-offs - đôi khi simple types đủ tốt
- Test branded type functions - ens’re type safety hoạt động