TypeScript Type Guards: Hướng dẫn xử lý kiểu dữ liệu an toàn cho dự án thực chiến

06 tháng 4, 2026·6 phút đọc

TypeScript Type Guards là công cụ giúp nhà phát triển thu hẹp các kiểu dữ liệu không chắc chắn thành các kiểu cụ thể, đảm bảo an toàn tại thời gian chạy. Bài viết sẽ khám phá các toán tử tích hợp sẵn, Custom Type Predicates và Discriminated Unions để xử lý dữ liệu phức tạp trong các hệ thống thanh toán.

TypeScript Type Guards: Hướng dẫn xử lý kiểu dữ liệu an toàn cho dự án thực chiến

Khi xây dựng một hệ thống thanh toán, sự "gần đúng" là chưa đủ. Chỉ một giá trị undefined hay một thuộc tính đối tượng không khớp cũng có thể là sự khác biệt giữa một giao dịch thành công và một khách hàng thất vọng (hoặc mất doanh thu).

Type Guards (Type Guards) của TypeScript chính là tuyến phòng thủ đầu tiên của bạn. Chúng cho phép bạn thu hẹp các kiểu dữ liệu rộng, không chắc chắn thành các kiểu cụ thể mà bạn có thể tương tác một cách an toàn. Trong hướng dẫn này, chúng ta sẽ xây dựng một bộ xử lý thanh toán thu nhỏ và học cách sử dụng Type Guards để làm cho nó "chống vỡ" (crash-proof).

1. Vấn đề: Những "Lỗi thầm lặng" của JavaScript

Hãy tưởng tượng bạn có một hàm xử lý các loại phản hồi thanh toán khác nhau. Trong JavaScript thuần, bạn có thể viết code như sau:

function processResponse(response) {
  // Nếu response là đối tượng 'Success', nó sẽ có 'transactionId'
  // Nếu là đối tượng 'Error', nó sẽ có 'message'
  console.log("Thanh toán thành công! ID: " + response.transactionId);
}

// Điều gì xảy ra nếu API trả về lỗi?
processResponse({ message: "Không đủ số dư" }); 
// Output: "Thanh toán thành công! ID: undefined" 

JavaScript không phàn nàn gì cả, nó chỉ trả về undefined. Đây là một "lỗi thầm lặng" (silent failure). TypeScript giúp chúng ta phát hiện điều này, nhưng chỉ khi chúng ta biết cách "thu hẹp" (narrow) các kiểu dữ liệu.

2. Các Type Guard tích hợp sẵn: Nền tảng cốt lõi

TypeScript cung cấp các toán tử tích hợp thực hiện kiểm tra tại thời gian chạy. Khi TypeScript nhìn thấy các kiểm tra này, nó sẽ "thu hẹp" kiểu dữ liệu cho phần còn lại của khối code đó.

typeof (Kiểm tra Kiểu nguyên thủy)

Trong ứng dụng thanh toán của chúng ta, số tiền (amount) có thể là number hoặc string (nếu nó đến từ đầu vào biểu mẫu).

function formatAmount(amount: string | number) {
  if (typeof amount === 'string') {
    // TypeScript biết 'amount' là string ở đây.
    // Chúng ta có thể gọi các phương thức riêng của string một cách an toàn.
    return parseFloat(amount).toFixed(2);
  }
  // Nếu đến được đây, TypeScript biết 'amount' PHẢI là một number.
  return amount.toFixed(2);
}

instanceof (Kiểm tra Class)

Giả sử bạn có các lớp khác nhau cho thanh toán CreditCard (Thẻ tín dụng) và GiftCard (Thẻ quà tặng). Mỗi loại có logic xác minh riêng.

class CreditCard {
  verifyCVV() { return true; }
}

class GiftCard {
  checkBalance() { return 50.00; }
}

function verifyPayment(method: CreditCard | GiftCard) {
  if (method instanceof CreditCard) {
    // An toàn để gọi phương thức của CreditCard
    method.verifyCVV();
  } else {
    // An toàn để gọi phương thức của GiftCard
    method.checkBalance();
  }
}

3. Custom Type Predicates: "Người gác cổng"

Đôi khi, các kiểm tra đơn giản là chưa đủ. Bạn có thể muốn tạo một hàm có thể tái sử dụng để kiểm tra xem một khoản thanh toán đã "hoàn tất" chưa. Để làm điều này, chúng ta sử dụng Custom Type Predicate.

Chúng ta giải thích kiểu trả về là value is Type. Điều này giống như người gác cổng tại một câu lạc bộ: hàm kiểm tra ID và nếu nó trả về true, TypeScript sẽ cho phép biến đó vào khối mã "đặc biệt" với kiểu dữ liệu cụ thể đó.

interface Payment {
  id: string;
  status: 'pending' | 'completed' | 'failed';
}

interface CompletedPayment extends Payment {
  status: 'completed';
  confirmedAt: Date;
}

// Đây là Custom Type Predicate của chúng ta
function isCompleted(payment: Payment): payment is CompletedPayment {
  return payment.status === 'completed';
}

const myPayment: Payment = { id: '123', status: 'completed' };

if (isCompleted(myPayment)) {
  // Giờ đây TypeScript biết myPayment có thuộc tính 'confirmedAt'!
  console.log(myPayment.confirmedAt);
}

4. Discriminated Unions: Tiêu chuẩn vàng

Nếu bạn chỉ học một mô hình từ bài viết này, hãy để nó là mô hình này. Bằng cách thêm một thuộc tính chung duy nhất (một "discriminator" - bộ phân biệt) vào các kiểu của bạn, bạn sẽ đạt được 100% sự an toàn về kiểu và tính năng gợi ý code (autocomplete) hoàn hảo của IDE.

interface CardPayment {
  type: 'card'; // Bộ phân biệt (Discriminator)
  lastFour: string;
}

interface PayPalPayment {
  type: 'paypal'; // Bộ phân biệt (Discriminator)
  email: string;
}

type PaymentMethod = CardPayment | PayPalPayment;

function getReceipt(method: PaymentMethod) {
  switch (method.type) {
    case 'card':
      return `Đã trừ tiền thẻ kết thúc bằng ${method.lastFour}`;
    case 'paypal':
      return `Đã trừ tài khoản PayPal: ${method.email}`;
  }
}

Tại sao điều này mạnh mẽ: Nếu sau này bạn thêm một loại thanh toán Bitcoin nhưng quên cập nhật câu lệnh switch này, TypeScript sẽ ngay lập tức đánh dấu lỗi. Nó giống như việc có một lập trình viên cấp cao đang nhìn chằm chằm vào vai bạn.

5. Anti-Patterns: "Trước và Sau"

Là người mới bắt đầu, bạn có thể bị cám dỗ để sử dụng các đường tắt để tắt các "dòng lỗi màu đỏ". Hãy xem tại sao bạn không nên làm vậy.

"Unsafe Assertion" (Khẳng định không an toàn)

Trước (Cách làm "bẩn"):

// Vượt qua kiểm tra an toàn với 'as any'
const receipt = (payment as any).cardNumber; 
// Nếu 'payment' thực tế là Chuyển khoản ngân hàng, kết quả sẽ là 'undefined'.

Sau (Cách làm "Guard"):

if ('cardNumber' in payment) {
  const receipt = payment.cardNumber; // An toàn và đã xác minh
}

Sử dụng toán tử in hoạt động như một khiên chắn khi xử lý dữ liệu không thể đoán trước từ các API bên ngoài.

Tóm tắt: Cây quyết định Type Guard của bạn

Chưa chắc chắn nên dùng Guard nào? Hãy làm theo danh sách kiểm tra này:

  1. Nó có phải là một giá trị đơn giản như chuỗi hoặc số không?
    • ✅ Sử dụng typeof.
  2. Nó có phải là một đối tượng được tạo với new MyClass() không?
    • ✅ Sử dụng instanceof.
  3. Các kiểu của bạn có thuộc tính chung như type hoặc status không?
    • ✅ Sử dụng Discriminated Unions (Tốt nhất cho logic phức tạp).
  4. Bạn có đang kiểm tra một thuộc tính cụ thể trên một đối tượng "lộn xộn" không?
    • ✅ Sử dụng toán tử in.
  5. Bạn có muốn một hàm có thể tái sử dụng để làm sạch các câu lệnh if của mình không?
    • ✅ Sử dụng Custom Type Predicate (is).

Kết luận

Type Guards là cầu nối giữa các quy tắc nghiêm ngặt của TypeScript và thực tế linh hoạt của JavaScript. Bằng cách triển khai các mô hình này trong hệ thống thanh toán của bạn, bạn không chỉ đang viết code—bạn đang xây dựng một động cơ đáng tin cậy, dễ dự đoán xử lý mọi trường hợp ngoại lệ một cách dễ dàng.

Chúc các bạn lập trình vui vẻ!

Tham khảo:

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗