NỘI DUNG BÀI HỌC
🔗 Hướng dẫn TypeScript Generics
📊 Đọc file XLSX trong Node.js bằng async/await
⚙️ Xử lý file JSON với TypeScript và fs/promises
📖 Cách dùng Record trong TypeScript để quản lý dữ liệu
🔗 Phần 1: Vì sao Generics sinh ra?
Trước khi nói về Generic là gì, cần nhìn đúng vấn đề mà nó giải quyết. Trong thực tế, bạn rất hay gặp những hàm có logic giống hệt nhau nhưng chỉ khác kiểu dữ liệu đầu vào và đầu ra.
Nếu không có Generic, bạn thường rơi vào một trong hai cách làm: hoặc viết rất nhiều hàm lặp lại, hoặc dùng any và đánh đổi toàn bộ sự an toàn kiểu dữ liệu.
🔹 1. Viết nhiều hàm theo từng kiểu cụ thể
Đây là cách dễ nghĩ ra nhất, nhưng càng đi xa thì càng lặp code.
function traVeSo(arg: number): number {
return arg; // Nhận số thì trả về số
}
function traVeChuoi(arg: string): string {
return arg; // Nhận chuỗi thì trả về chuỗi
}
// Nếu cần thêm boolean, object, array...
// bạn lại phải viết thêm nhiều hàm gần như giống hệt nhau
Vấn đề ở đây không phải logic khó, mà là logic giống nhau nhưng bị nhân bản theo kiểu dữ liệu.
🔹 2. Dùng any để “chấp nhận tất cả”
Cách này nhìn qua có vẻ tiện, nhưng thật ra lại làm mất đi giá trị lớn nhất của TypeScript.
function traVeGiaTriBatKy(arg: any): any {
return arg; // Nhận gì trả lại nấy
}
const output = traVeGiaTriBatKy("Xin chào");
// TypeScript không biết output là string
// output.toFixed(2); // Không bị chặn tốt như mong đợi, dễ lỗi runtime
console.log(output.length); // Chạy được ở đây chỉ là do "may mắn"
Dùng any giúp hàm dùng được với mọi kiểu, nhưng đổi lại TypeScript gần như “mù” về kiểu dữ liệu. Đó chính là lý do Generic xuất hiện.
🧠 Phần 2: Generic là gì và cú pháp dùng thế nào?
Bạn có thể hình dung Generic như một “cái khuôn” hoặc một “cái chai rỗng”. Bản thân cái khuôn đó không cố định dữ liệu gì bên trong, nhưng một khi bạn đổ dữ liệu cụ thể vào, TypeScript sẽ nhớ chính xác kiểu dữ liệu đó từ đầu tới cuối.
Ký hiệu quen thuộc nhất là <T>, trong đó T là một biến kiểu. Ngoài T, bạn còn hay gặp U, K, V tùy ngữ cảnh.
🔹 1. Generic trong hàm với nhiều kiểu đầu vào
function taoCapGiaTri<T, U>(key: T, value: U): { key: T; value: U } {
return { key, value }; // Giữ nguyên kiểu của cả key và value
}
const cap1 = taoCapGiaTri<string, number>("tuoi", 30);
console.log(cap1.key); // string
console.log(cap1.value); // number
Đây là cú pháp rất quan trọng: phần <T, U> đứng ngay sau tên hàm, rồi mới tới danh sách tham số.
🔹 2. Generic function cơ bản và type inference
TypeScript thường đủ thông minh để tự suy luận kiểu khi bạn gọi hàm, nên không phải lúc nào cũng cần viết tường minh <T>.
function identity<T>(arg: T): T {
return arg; // Nhận kiểu gì thì trả về đúng kiểu đó
}
const outputString = identity<string>("Chào TypeScript!");
console.log(outputString.toUpperCase()); // TypeScript biết đây là string
const outputNumber = identity(100); // Tự suy luận T là number
console.log(outputNumber.toFixed(2));
const outputBoolean = identity(true); // Tự suy luận T là boolean
console.log(!outputBoolean);
🔹 3. Generic với mảng
function getFirstElement<T>(arr: T[]): T | undefined {
return arr[0]; // Lấy phần tử đầu tiên của mảng
}
const firstNum = getFirstElement([1, 2, 3]); // number | undefined
const firstStr = getFirstElement(["a", "b", "c"]); // string | undefined
console.log(firstNum);
console.log(firstStr);
Hàm này dùng tốt với mảng số, mảng chuỗi, mảng object hay bất kỳ kiểu dữ liệu nào khác mà không cần viết lại.
🔹 4. Generic Interface cho dữ liệu API
Đây là một ứng dụng cực kỳ thực tế. Rất nhiều API có cùng “vỏ” response, chỉ khác phần data bên trong.
interface ApiResponse<T> {
success: boolean;
message: string;
data: T; // T có thể là bất kỳ kiểu nào phù hợp
}
interface User {
id: number;
name: string;
email: string;
}
interface Product {
sku: string;
name: string;
price: number;
}
const userResponse: ApiResponse<User> = {
success: true,
message: "Lấy thông tin người dùng thành công!",
data: {
id: 1,
name: "Gemini",
email: "gemini@google.com"
}
};
const productResponse: ApiResponse<Product[]> = {
success: true,
message: "Lấy danh sách sản phẩm thành công!",
data: [
{ sku: "TS-001", name: "Áo phông TypeScript", price: 250000 },
{ sku: "JS-002", name: "Cốc cà phê JavaScript", price: 150000 }
]
};
🔹 5. Generic Class để tái sử dụng cấu trúc
class KhoHang<T> {
private items: T[] = [];
themMatHang(item: T): void {
this.items.push(item); // Chỉ thêm đúng kiểu T đã khai báo
}
layMatHang(): T | undefined {
return this.items.pop();
}
}
interface DienThoai {
model: string;
}
const khoDienThoai = new KhoHang<DienThoai>();
khoDienThoai.themMatHang({ model: "iPhone 17 Pro" });
const dt = khoDienThoai.layMatHang();
console.log(dt?.model);
Khi class đã được “đóng khuôn” bằng một kiểu cụ thể, mọi method bên trong sẽ tuân theo kiểu đó rất chặt.
🔹 6. Generic Constraints với extends
Không phải lúc nào generic cũng được phép nhận “bất cứ thứ gì”. Đôi khi bạn cần ràng buộc rằng kiểu truyền vào phải có một số thuộc tính nhất định.
interface CoTheInTen {
name: string;
}
function inThongTin<T extends CoTheInTen>(item: T): void {
console.log(`Xin chào, tên tôi là ${item.name}`);
}
const nguoi = { name: "Gemini", age: 1 };
const xe = { name: "Ford Ranger", color: "Xám" };
const mayTinh = { brand: "Dell", price: 2000 };
inThongTin(nguoi); // OK
inThongTin(xe); // OK
// inThongTin(mayTinh); // Lỗi vì object này không có thuộc tính name
Đây là điểm rất mạnh của Generic: vừa linh hoạt, vừa không buông lỏng kiểu dữ liệu một cách vô kiểm soát.
📄 Phần 3: Đọc file JSON với fs/promises và async/await
Bài này chuyển từ Generic sang một ứng dụng rất thực chiến: đọc dữ liệu từ file. Với tester và automation engineer, đây là chuyện xảy ra thường xuyên vì bạn phải làm việc với test data, config, mock response, user seed data...
JSON là định dạng văn bản nên khá dễ xử lý. Luồng cơ bản là: đọc file → chuyển Buffer thành string → JSON.parse().
🔹 1. Ví dụ nội dung file users.json
[
{
"id": 1,
"name": "An",
"role": "Admin"
},
{
"id": 2,
"name": "Binh",
"role": "Member"
}
]
🔹 2. Hàm đọc JSON bằng Generic
Điểm hay ở đây là bạn có thể kết hợp luôn Generic để hàm đọc file trả về kiểu dữ liệu chính xác.
import { readFile } from "fs/promises";
import { resolve } from "path";
async function readJsonFile<T>(filePath: string): Promise<T> {
try {
console.log(`Đang đọc file JSON từ: ${filePath}`);
const fileBuffer = await readFile(filePath); // Đọc file dưới dạng Buffer
const jsonString = fileBuffer.toString("utf-8"); // Chuyển sang chuỗi
const data: T = JSON.parse(jsonString); // Parse JSON và ép kiểu bằng Generic
console.log("Đọc file JSON thành công!");
return data;
} catch (error) {
console.error("Đã xảy ra lỗi khi đọc file JSON:", error);
throw error; // Ném lại lỗi để nơi gọi bên ngoài xử lý tiếp
}
}
interface User {
id: number;
name: string;
role: string;
}
async function main(): Promise<void> {
const users = await readJsonFile<User[]>(resolve("users.json"));
users.forEach((user) => {
console.log(`- ${user.name} (Role: ${user.role})`);
});
}
main();
💡 Mẹo nhớ nhanh: JSON là text, nên công thức gần như luôn là
readFile→toString()→JSON.parse().
📊 Phần 4: Đọc file XLSX bằng thư viện xlsx
Khác với JSON, file XLSX không phải văn bản thuần. Nó là định dạng nén chứa nhiều XML bên trong, nên bạn không thể chỉ dùng fs rồi parse như JSON được. Cách thực tế nhất là dùng thư viện chuyên dụng như xlsx.
🔹 1. Cài thư viện cần thiết
npm install xlsx
🔹 2. Luồng xử lý XLSX
- Đọc workbook: dùng
XLSX.readFile(). - Lấy tên sheet: thường là sheet đầu tiên hoặc sheet bạn chỉ định.
- Lấy worksheet: từ
workbook.Sheets[sheetName]. - Chuyển sang object: dùng
XLSX.utils.sheet_to_json().
🔹 3. Hàm đọc file Excel bằng Generic
import * as XLSX from "xlsx";
import { resolve } from "path";
async function readXlsxFile<T>(filePath: string): Promise<T[]> {
try {
console.log(`Đang đọc file XLSX từ: ${filePath}`);
const workbook = XLSX.readFile(filePath); // Đọc toàn bộ workbook
const firstSheetName = workbook.SheetNames[0]; // Lấy sheet đầu tiên
if (!firstSheetName) {
throw new Error("File Excel không có sheet nào.");
}
const worksheet = workbook.Sheets[firstSheetName]; // Lấy worksheet theo tên sheet
const data: T[] = XLSX.utils.sheet_to_json<T>(worksheet); // Chuyển sheet thành mảng object
console.log("Đọc file XLSX thành công!");
return data;
} catch (error) {
console.error("Đã xảy ra lỗi khi đọc file XLSX:", error);
throw error;
}
}
interface Product {
id: number;
productName: string;
price: number;
inStock: boolean;
}
async function mainExcel(): Promise<void> {
const products = await readXlsxFile<Product>(resolve("products.xlsx"));
products.forEach((product) => {
const stockStatus = product.inStock ? "Còn hàng" : "Hết hàng";
console.log(
`- ${product.productName} | Giá: ${product.price.toLocaleString()}đ | Trạng thái: ${stockStatus}`
);
});
}
mainExcel();
Trong thực tế test data, cách làm này rất hữu ích vì team QA hay lưu dữ liệu đầu vào dưới dạng Excel. Khi đã đọc lên thành mảng object typed, bạn có thể dùng lại cực dễ trong test case.
⚠️ Lưu ý: Với file I/O, hãy ưu tiên
async/awaitđi kèmtry...catchđể log lỗi rõ ràng và tránh flow khó đọc.
📖 Phần 5: Bonus Record<Keys, Type> - định kiểu cho object “động”
Record là một Utility Type rất hữu ích khi bạn cần mô tả các object có key động nhưng value lại tuân theo cùng một kiểu dữ liệu. Đây là tình huống rất thường gặp khi lưu config, gom nhóm dữ liệu hoặc map quyền hạn theo role.
Cú pháp tổng quát là: Record<Keys, Type>.
🔹 1. Feature flags với Record<string, boolean>
const featureFlags: Record<string, boolean> = {
darkMode: true,
newHomePage: false,
betaFeature: true
};
const isDarkMode = featureFlags["darkMode"]; // TypeScript hiểu đây là boolean
featureFlags["experimentalAnalytics"] = true; // Có thể mở rộng thêm key mới
// featureFlags.newHomePage = "false"; // Lỗi: value phải là boolean
🔹 2. Gom nhóm dữ liệu để tra cứu nhanh
interface ProductItem {
id: number;
name: string;
categoryId: string;
}
type ProductsByCategory = Record<string, ProductItem[]>;
const products: ProductItem[] = [
{ id: 1, name: "Laptop Pro", categoryId: "electronics" },
{ id: 2, name: "Bàn phím cơ", categoryId: "electronics" },
{ id: 3, name: "Tiểu thuyết Z", categoryId: "books" }
];
const groupedProducts: ProductsByCategory = {};
products.forEach((product) => {
const category = product.categoryId;
if (!groupedProducts[category]) {
groupedProducts[category] = []; // Khởi tạo mảng nếu category chưa tồn tại
}
groupedProducts[category].push(product); // Thêm sản phẩm vào nhóm tương ứng
});
Đây là pattern rất hợp cho các bài toán cần nhóm test data, gom API data theo nhóm hoặc tạo object lookup nhanh.
🔹 3. Giới hạn chặt key bằng union type
type UserRole = "admin" | "editor" | "viewer";
interface Permissions {
canRead: boolean;
canWrite: boolean;
canDelete: boolean;
}
const rolePermissions: Record<UserRole, Permissions> = {
admin: { canRead: true, canWrite: true, canDelete: true },
editor: { canRead: true, canWrite: true, canDelete: false },
viewer: { canRead: true, canWrite: false, canDelete: false }
};
// rolePermissions.guest = { canRead: true, canWrite: false, canDelete: false };
// Lỗi: "guest" không nằm trong UserRole
// rolePermissions.viewer = { canRead: true };
// Lỗi: thiếu canWrite và canDelete
🔹 Ghi nhớ nhanh cho Tester
- Generic giúp viết một logic chung mà vẫn giữ được kiểu dữ liệu cụ thể cho từng lần sử dụng.
- Đừng dùng
anyđể thay Generic nếu bạn vẫn muốn giữ type safety. ApiResponse<T>,KhoHang<T>,readJsonFile<T>là các pattern generic rất thực dụng.- JSON là text nên luồng xử lý thường là
readFile→toString→JSON.parse. - XLSX cần thư viện chuyên dụng như
xlsx, rồi dùngsheet_to_jsonđể đổi sang mảng object. Recordrất hợp khi bạn cần mô tả object dạng dictionary, map cấu hình hoặc gom nhóm dữ liệu.- Khi cần ép generic phải có thuộc tính cụ thể, hãy dùng ràng buộc với
extends.
✅ Kết luận: Bài 12 là bước rất thực chiến của TypeScript. Khi bạn hiểu Generic không chỉ ở mức cú pháp mà còn biết áp dụng nó vào đọc JSON, XLSX và quản lý object động bằng Record, bạn đã tiến gần hơn tới việc xây những utility và framework automation thật sự dùng được trong dự án.
