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:
- Overload signatures — khai báo các kiểu input/output
- 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: stringQuy 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: stringLư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
anyunion — 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.