Bài 5. Union, narrowing và discriminated union

Union cho phép một giá trị thuộc một trong nhiều kiểu. Narrowing là kỹ thuật thu hẹp kiểu để dùng an toàn. Đây là kỹ năng quan trọng nhất khi viết TypeScript thực tế.

45 phútTrung cấp
Bạn sẽ học được
  • Khai báo và dùng union type
  • Thu hẹp kiểu bằng typeof, in, instanceof, so sánh
  • Thiết kế discriminated union
  • Kiểm tra vét cạn với never
Thuật ngữ cần biết
Union (|)
Giá trị thuộc một trong các kiểu.
Narrowing
TS tự thu hẹp kiểu sau câu lệnh kiểm tra.
Discriminant
Thuộc tính chung có giá trị literal khác nhau để phân biệt nhánh.
Exhaustive check
Đảm bảo đã xử lý mọi trường hợp.

Khái niệm và lý thuyết

Với union string | number, bạn chỉ được dùng các thao tác chung của cả hai kiểu. Muốn dùng phương thức riêng, phải thu hẹp. TS phân tích luồng điều khiển (control flow analysis): sau if (typeof x === "string"), trong nhánh đó x là string, ở nhánh else x là number.

Discriminated union là mẫu thiết kế cốt lõi: mỗi biến thể có thuộc tính chung (ví dụ kind) mang giá trị literal riêng. Mẫu này mô hình hóa trạng thái (đang tải, thành công, lỗi) rất chính xác, loại bỏ các tổ hợp trạng thái vô nghĩa.

Các cách thu hẹp

CáchVí dụDùng cho
typeoftypeof x === "string"Kiểu nguyên thủy
Truthinessif (x)Loại null/undefined/""
in"bay" in conPhân biệt object theo thuộc tính
instanceofe instanceof DateĐối tượng tạo từ lớp
So sánhx.kind === "loi"Discriminated union
function inId(id: string | number) {
  if (typeof id === "string") console.log(id.toUpperCase());
  else console.log(id.toFixed(0));
}

Discriminated union

type KetQua =
  | { trangThai: "dang-tai" }
  | { trangThai: "thanh-cong"; duLieu: string[] }
  | { trangThai: "loi"; thongBao: string };

function hienThi(kq: KetQua): string {
  switch (kq.trangThai) {
    case "dang-tai":   return "Đang tải...";
    case "thanh-cong": return kq.duLieu.join(", ");
    case "loi":        return "Lỗi: " + kq.thongBao;
    default: {
      const _vetCan: never = kq; // báo lỗi nếu quên một nhánh
      return _vetCan;
    }
  }
}

Khi thêm biến thể mới vào KetQua, dòng never sẽ báo lỗi ở mọi chỗ chưa xử lý: trình biên dịch nhắc việc thay bạn.

Tình huống: Thanh toán nhiều phương thức

Mô hình ThanhToan gồm tiền mặt (số tiền), chuyển khoản (số tiền, mã giao dịch), ví điện tử (số tiền, tên ví). Hàm tính phí xử lý theo từng loại dùng switch và kiểm tra vét cạn.

Mẹo hay

Đặt tên discriminant thống nhất trong dự án (type, kind hoặc status) để đồng đội dễ nhận ra mẫu.

Lưu ý

Narrowing bị mất trong callback nếu biến có thể bị gán lại (let). Gán vào một const trước khi dùng trong callback.

Thực hành
  1. Viết hàm doDai(x: string | string[]) trả về số ký tự hoặc số phần tử.
  2. Mô hình hình học Hinh (tròn, chữ nhật, tam giác) và hàm tính diện tích.
  3. Thêm biến thể “vuông” và quan sát lỗi never.
  4. Mô hình trạng thái gửi biểu mẫu: chưa gửi, đang gửi, đã gửi, lỗi.
Tóm tắt bài học
  • Union mô tả “một trong nhiều”.
  • Thu hẹp bằng typeof, in, instanceof, so sánh.
  • Discriminated union mô hình hóa trạng thái chính xác.
  • never giúp kiểm tra vét cạn.