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/promisesasync/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à readFiletoString()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èm try...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à readFiletoStringJSON.parse.
  • XLSX cần thư viện chuyên dụng như xlsx, rồi dùng sheet_to_json để đổi sang mảng object.
  • Record rấ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.

Teacher

Teacher

Nguyên Hoàng

Automation Engineer

With 7+ years of hands-on experience across multiple languages and frameworks. I'm here to share knowledge, helping you turn complex processes into simple and effective solutions.

Cộng đồng Automation Testing Việt Nam:

🌱 Telegram Automation Testing:   Cộng đồng Automation Testing
🌱 
Facebook Group Automation: Cộng đồng Automation Testing Việt Nam
🌱 
Facebook Fanpage: Cộng đồng Automation Testing Việt Nam - Selenium
🌱 Telegram
Manual Testing:   Cộng đồng Manual Testing
🌱 
Facebook Group Manual: Cộng đồng Manual Testing Việt Nam

Chia sẻ khóa học lên trang

Bạn có thể đăng khóa học của chính bạn lên trang Anh Tester để kiếm tiền

Danh sách bài học