Function Overloads & Generic Inference

Bài 11: Function Overloads & Generic Inference

Bạn đã từng viết một function mà input khác nhau thì output cũng khác nhau chưa? Ví dụ: truyền string thì trả về string, truyền number thì trả về number. Với union types, TypeScript thường "mất" type cụ thể. Đây là lúc function overloads phát huy tác dụng.


#Vấn đề: Union type mất type information

// ❌ TypeScript không biết input type nào → output type nào
function parse(input: string): object;
function parse(input: number): object;
// Nhưng viết kiểu này thì không compile được — phải dùng overloads đúng cách
 
// ❌ Dùng union — mất type ở output
function getValue(key: string): string | number {
  const data: Record<string, string | number> = {
    name: "An",
    age: 25,
  };
  return data[key];
}
 
const name = getValue("name"); // type: string | number 😢
// Phải casting: name as string — không an toàn
// ✅ Dùng overload — type chính xác ở output
function getValue(key: "name"): string;
function getValue(key: "age"): number;
function getValue(key: string): string | number {
  const data: Record<string, string | number> = {
    name: "An",
    age: 25,
  };
  return data[key];
}
 
const name = getValue("name"); // ✅ type: string
const age = getValue("age"); // ✅ type: number

#Function Overloads — Cấu trúc đúng

Một function overload có 2 phần:

  1. Overload signatures — khai báo các kiểu input/output
  2. Implementation signature — phần code thật
// Overload signatures (chỉ khai báo, không có body)
function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: "span"): HTMLSpanElement;
function createElement(tag: "input"): HTMLInputElement;
function createElement(tag: string): HTMLElement;
 
// Implementation signature (phải compatible với tất cả signatures trên)
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

Quy tắc quan trọng:

  • Implementation signature không được gọi trực tiếp
  • Phải có ít nhất 1 overload signature
  • Implementation phải compatible với mọi overload signature
// ❌ Lỗi: implementation signature không visible từ bên ngoài
createElement("canvas"); // Error: No overload matches this call
 
// ✅ Phải thêm overload signature cho "canvas"
function createElement(tag: "canvas"): HTMLCanvasElement;
function createElement(tag: string): HTMLElement;
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

#Overloads vs Generics — Khi nào dùng cái nào?

#Dùng Generics khi: output type phụ thuộc vào input type một cách tự nhiên

// ✅ Generic — đơn giản, rõ ràng
function identity<T>(value: T): T {
  return value;
}
 
const str = identity("hello"); // ✅ type: string
const num = identity(42); // ✅ type: number

#Dùng Overloads khi: input type khác nhau → logic khác nhau

// ✅ Overload — mỗi input type có xử lý khác nhau
function format(value: Date): string;
function format(value: number): string;
function format(value: string): string;
function format(value: Date | number | string): string {
  if (value instanceof Date) {
    return value.toISOString();
  }
  if (typeof value === "number") {
    return value.toFixed(2);
  }
  return value.trim();
}
 
format(new Date()); // ✅ "2024-01-15T..."
format(3.14159); // ✅ "3.14"
format("  hello  "); // ✅ "hello"

#Dùng kết hợp cả hai

// ✅ Overload + Generic
function toArray<T>(value: T): T[];
function toArray<T>(value: T[]): T[];
function toArray<T>(value: T | T[]): T[] {
  return Array.isArray(value) ? value : [value];
}
 
toArray("hello"); // ✅ string[]
toArray([1, 2, 3]); // ✅ number[]

#Thứ tự Inference — Quy tắc quan trọng

TypeScript resolve overloads từ trên xuống. Overload đầu tiên match sẽ được chọn.

// ❌ Thứ tự sai — overload rộng match trước
function process(value: string): string;
function process(value: "specific"): "result"; // Không bao giờ được chọn!
function process(value: string): string {
  return value;
}
 
process("specific"); // type: string (không phải "result")
// ✅ Thứ tự đúng — overload cụ thể match trước
function process(value: "specific"): "result";
function process(value: string): string;
function process(value: string): string {
  return value;
}
 
process("specific"); // ✅ type: "result"
process("other"); // ✅ type: string

Quy tắc: Luôn đặt overload cụ thể trước, overload rộng sau.


#Overloads trong Method

class EventBus {
  // ❌ Anti-pattern: dùng any union thay vì overloads
  on(event: string, handler: (data: any) => void): void {
    // any = mất type safety
  }
 
  // ✅ Overloads cho từng event
  on(event: "user:login", handler: (data: { userId: string }) => void): void;
  on(event: "user:logout", handler: (data: { userId: string }) => void): void;
  on(event: "error", handler: (data: { code: number; message: string }) => void): void;
  on(event: string, handler: (data: unknown) => void): void {
    // implementation
  }
}
 
const bus = new EventBus();
bus.on("user:login", (data) => {
  console.log(data.userId); // ✅ TypeScript biết data có userId
});
 
bus.on("error", (data) => {
  console.log(data.code, data.message); // ✅ TypeScript biết data có code và message
});

#Overloads với Conditional Return Types

function fetchData<T extends boolean>(
  url: string,
  parse: T
): T extends true ? object : string;
function fetchData(url: string, parse: boolean): object | string {
  const response = "raw data";
  return parse ? JSON.parse(response) : response;
}
 
const parsed = fetchData("/api", true); // ✅ type: object
const raw = fetchData("/api", false); // ✅ type: string

Lưu ý: Conditional return types với overloads cần implementation signature. Implementation trả về union type.


#Anti-pattern: any union thay vì overloads

// ❌ Anti-pattern
function convert(input: string | number): string | number {
  if (typeof input === "string") return Number(input);
  return String(input);
}
 
const result = convert("42"); // type: string | number — mất type info!
// ✅ Overloads
function convert(input: string): number;
function convert(input: number): string;
function convert(input: string | number): string | number {
  if (typeof input === "string") return Number(input);
  return String(input);
}
 
const result = convert("42"); // ✅ type: number

#Real-world: API Client với Overloads

class ApiClient {
  // GET — trả về data
  request<T>(method: "GET", url: string): Promise<T>;
 
  // POST/PUT — trả về data, cần body
  request<T>(method: "POST" | "PUT", url: string, body: T): Promise<T>;
 
  // DELETE — trả về void
  request(method: "DELETE", url: string): Promise<void>;
 
  // Implementation
  async request(method: string, url: string, body?: unknown): Promise<unknown> {
    const options: RequestInit = { method };
    if (body) {
      options.body = JSON.stringify(body);
      options.headers = { "Content-Type": "application/json" };
    }
    const response = await fetch(url, options);
    if (method === "DELETE") return;
    return response.json();
  }
}
 
const api = new ApiClient();
 
// ✅ TypeScript biết chính xác type
const users = await api.request<User[]>("GET", "/api/users"); // type: User[]
const newUser = await api.request<User>("POST", "/api/users", { name: "An" }); // type: User
await api.request("DELETE", "/api/users/1"); // type: void

#Bài tập

#Dễ

Viết overload cho function stringify nhận number trả về string, nhận boolean trả về "true" | "false".

#Trung bình

Viết overload cho function getProperty(obj, key) mà trả về type chính xác của property tương ứng.

#Khó

Viết class QueryBuilder với method where có overload: where(field, value) exact match và where(field, operator, value) với operator là "gt" | "lt" | "eq".


#Tổng kết

  • Function overloads cho phép nhiều signatures cho cùng một function
  • Overload signatures khai báo type, implementation signature chứa code thật
  • Đặt overload cụ thể trước, overload rộng sau (trên xuống dưới)
  • Dùng generics khi output type phụ thuộc input type đơn giản
  • Dùng overloads khi input type khác nhau dẫn đến logic khác nhau
  • Không dùng any union — hãy dùng overloads để giữ type safety

Bài tiếp theo: Builder Pattern type-safe — cách xây dựng fluent API giữ type qua từng method chain.