0

Series Total TypeScript Thực chiến #8: Function Overloads – "Đa hình" hàm trong Backend

1. Nỗi đau của hàm "Đa nhân cách"

Giả sử trong hệ thống lõi AFC, bạn cần viết một hàm để truy vấn thông tin vé (Ticket). Yêu cầu nghiệp vụ như sau:

  • Nếu truyền vào một chuỗi (String đại diện cho Ticket ID), hàm trả về một Object vé duy nhất.
  • Nếu truyền vào một mảng các chuỗi (Array các Ticket IDs), hàm trả về một mảng các Object vé.

Nếu bạn viết bằng Union Types thông thường, code sẽ trông thế này:

type Ticket = { id: string; price: number; status: string };

// ❌ TypeScript bị bối rối ở đầu ra
function getTicket(input: string | string[]): Ticket | Ticket[] {
  if (typeof input === 'string') {
    return { id: input, price: 15000, status: 'ACTIVE' }; // Trả về 1 vé
  } else {
    return input.map(id => ({ id, price: 15000, status: 'ACTIVE' })); // Trả về mảng vé
  }
}

Vấn đề xuất hiện ngay khi bạn gọi hàm:

const singleTicket = getTicket('TICKET_001');

// ❌ IDE báo lỗi khi bạn cố lấy id
// Property 'id' does not exist on type 'Ticket | Ticket[]'.
console.log(singleTicket.id);

TypeScript không đủ thông minh để hiểu rằng vì bạn truyền vào string nên kết quả CHẮC CHẮN là Ticket. Nó chỉ nhìn vào chữ ký hàm và kết luận kết quả có thể là mảng. Để code chạy được, bạn lại phải ép người dùng viết lệnh if (!Array.isArray(singleTicket)) rất mất thời gian.

2. Thuốc giải: Cú pháp Function Overloads

Để giải quyết, TypeScript cho phép bạn khai báo chữ ký hàm (signatures) nhiều lần trước khi thực sự viết logic (implementation) của hàm đó.

Đây là cách chúng ta thiết lập luật chơi rõ ràng:

// 1. Chữ ký thứ nhất: Nếu đầu vào là string, đầu ra CHẮC CHẮN là Ticket
function getTicket(id: string): Ticket;

// 2. Chữ ký thứ hai: Nếu đầu vào là string[], đầu ra CHẮC CHẮN là Ticket[]
function getTicket(ids: string[]): Ticket[];

// 3. Logic thực thi (Implementation) - Phải bao quát được mọi trường hợp
function getTicket(input: string | string[]): Ticket | Ticket[] {
  if (typeof input === 'string') {
    return { id: input, price: 15000, status: 'ACTIVE' };
  } else {
    return input.map(id => ({ id, price: 15000, status: 'ACTIVE' }));
  }
}

Phép màu xảy ra khi bạn gọi hàm:

// ✅ IDE tự động chọn chữ ký thứ nhất. singleTicket mang type chuẩn xác là Ticket.
const singleTicket = getTicket('TICKET_001');
console.log(singleTicket.id); // Ngon lành!

// ✅ IDE tự động chọn chữ ký thứ hai. multiTickets mang type chuẩn xác là Ticket[].
const multiTickets = getTicket(['TICKET_001', 'TICKET_002']);
console.log(multiTickets.length); // Ngon lành!

3. Overloads với các số lượng tham số khác nhau

Không chỉ khác nhau về Type, Overloads còn cực kỳ hữu dụng khi bạn muốn một hàm có thể nhận số lượng tham số linh hoạt.

Ví dụ, hàm khởi tạo thời gian cho giao dịch. Bạn có thể truyền vào Timestamp (1 tham số), hoặc truyền vào Ngày, Tháng, Năm (3 tham số):

// Chữ ký 1: Nhận vào 1 số (Timestamp)
function createTransactionTime(timestamp: number): Date;

// Chữ ký 2: Nhận vào 3 số (Năm, Tháng, Ngày)
function createTransactionTime(year: number, month: number, day: number): Date;

// Logic thực thi: Tham số thứ 2 và 3 phải để tùy chọn (?) vì Chữ ký 1 không có chúng
function createTransactionTime(arg1: number, arg2?: number, arg3?: number): Date {
  if (arg2 !== undefined && arg3 !== undefined) {
    // Luồng cho 3 tham số (Lưu ý: month trong JS bắt đầu từ 0)
    return new Date(arg1, arg2 - 1, arg3);
  } else {
    // Luồng cho 1 tham số
    return new Date(arg1);
  }
}

const timeFromTimestamp = createTransactionTime(1715421000000);
const timeFromDate = createTransactionTime(2026, 7, 11);

// ❌ Báo lỗi nếu truyền sai số lượng (ví dụ truyền 2 tham số)
// const errorTime = createTransactionTime(2026, 7);

4. Khi nào dùng Overloads, khi nào dùng Generics?

Rất nhiều kỹ sư nhầm lẫn và lạm dụng Generics cho những bài toán không cần thiết. Quy tắc chuẩn Senior (mà Matt Pocock luôn nhấn mạnh) như sau:

  • Dùng Generics: Khi Type của đầu ra có mối quan hệ phụ thuộc tuyến tính vào Type của đầu vào. (Ví dụ: Hàm trả về chính tham số đầu vào, hoặc bọc tham số đầu vào trong một Promise).
  • Dùng Overloads: Khi logic bên trong hàm thay đổi rẽ nhánh (if/else) dựa trên Type hoặc số lượng của tham số đầu vào, và mỗi nhánh trả về một cấu trúc dữ liệu hoàn toàn khác biệt.

Bằng việc thành thạo Function Overloads, bạn đang tạo ra một trải nghiệm (Developer Experience - DX) tuyệt vời cho chính mình và đồng nghiệp. Các hàm tiện ích (Utility) giờ đây tự động gợi ý đúng Type, đúng ngữ cảnh mà không cần phải viết những đoạn ép kiểu as đầy rủi ro.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí