NỘI DUNG BÀI HỌC

🏗️ Kế thừa & Interfaces (extends, implements)

✅ Quản lý Hằng số với Enum

🧩 Linh hoạt hóa với Generic



PHẦN 1: KẾ THỪA VÀ "HỢP ĐỒNG" TRONG TYPESCRIPT (extends & implements)

Trong lập trình hướng đối tượng (OOP), việc tổ chức và tái sử dụng mã nguồn một cách hiệu quả là chìa khóa để xây dựng các hệ thống lớn, dễ bảo trì. TypeScript cung cấp hai từ khóa cực kỳ mạnh mẽ để thực hiện điều này: extends để kế thừa và implements để tuân thủ "hợp đồng". Hãy cùng đào sâu vào từng khái niệm và xem cách chúng biến đổi cách chúng ta viết code automation.

Kế thừa Class với extends - Mối quan hệ "LÀ MỘT" (IS-A)

Kế thừa (extends) cho phép một lớp con (subclass) thừa hưởng lại các thuộc tính và phương thức của một lớp cha (superclass). Đây là cách để chia sẻ mã nguồn và tạo ra một hệ thống phân cấp logic.

Mối quan hệ cốt lõi: Hãy nghĩ về extends như mối quan hệ "IS-A" (Là một). Ví dụ, một HomePage LÀ MỘT BasePage. Một Chó LÀ MỘT ĐộngVật. Mối quan hệ này ngụ ý rằng lớp con là một phiên bản chuyên biệt hơn của lớp cha.

Cú pháp và Các Thành phần Chính

extends: Từ khóa để thiết lập mối quan hệ kế thừa.

super():

Đây là một lời gọi hàm đặc biệt trong constructor của lớp con, dùng để thực thi constructor của lớp cha.

Bắt buộc phải gọi super() trước khi sử dụng this trong constructor của lớp con. Lý do là vì lớp cha cần được "xây dựng" trước, để các thuộc tính và phương thức của nó sẵn sàng cho lớp con sử dụng.

🧠 Ví von sâu hơn: super() không chỉ là "Thưa bố, con đã ở đây!". Nó giống như việc người con nói: "Bố ơi, hãy giúp con xây dựng phần nền móng chung (khởi tạo this.page), sau đó con sẽ tự trang trí thêm phòng riêng của mình (khởi tạo this.welcomeMessage)".

Kiểm soát Truy cập trong Kế thừa: public, protected, private

public: Thuộc tính/phương thức có thể được truy cập từ bất cứ đâu.

private: Chỉ có thể truy cập bên trong chính class đó. Lớp con không thể truy cập.

protected: Có thể truy cập bên trong class đó và bên trong các lớp con kế thừa nó. Đây là lựa chọn hoàn hảo cho các thuộc tính dùng chung trong BasePage.


Ghi đè phương thức (Method Overriding)

Lớp con không chỉ kế thừa mà còn có thể định nghĩa lại một phương thức của lớp cha để thay đổi hành vi của nó.

Ứng dụng nâng cao trong Automation: Xây dựng BasePage linh hoạt

Đây là nền tảng của mô hình Page Object Model (POM) để loại bỏ lặp code và quản lý tập trung.

 

// LỚP CHA: Chứa các hành động và thuộc tính chung cho MỌI TRANG

abstract class BasePage {

  // Dùng protected để chỉ lớp con mới có thể truy cập trực tiếp

  protected readonly page: any; // Dùng readonly để đảm bảo page không bị gán lại sau khi khởi tạo

  constructor(page: any) {

    this.page = page;

  }


  // Một hành động chung

  async navigateTo(url: string): Promise<void> {

    await this.page.goto(url);

  }


  // Một phương thức chung để lấy thông tin

  async getTitle(): Promise<string> {

    console.log("Lấy title từ BasePage...");

    return await this.page.title();

  }

  async takeScreenshot(fileName: string): Promise<void> {

    await this.page.screenshot({ path: `screenshots/${fileName}.png` });

  }

}

// LỚP CON 1: HomePage

class HomePage extends BasePage {

  public welcomeMessage: any;

  constructor(page: any) {

    // Bắt buộc gọi super() đầu tiên để khởi tạo 'this.page'

    super(page);   

    // Khởi tạo các thuộc tính riêng của HomePage

    this.welcomeMessage = page.locator('.welcome-message');

  }

  async getWelcomeText(): Promise<string> {

    return await this.welcomeMessage.textContent();

  }

}

// LỚP CON 2: LoginPage kế thừa và GHI ĐÈ phương thức

class LoginPage extends BasePage {

  public usernameInput: any;

  public passwordInput: any;

  constructor(page: any) {

    super(page);

    this.usernameInput = page.locator('#username');

    this.passwordInput = page.locator('#password');

  }

  async login(user: string, pass: string): Promise<void> {

    await this.usernameInput.fill(user);

    await this.passwordInput.fill(pass);

    // ... click login button

  }

  // GHI ĐÈ (OVERRIDE) phương thức của lớp cha

  async getTitle(): Promise<string> {

    // Gọi lại phương thức gốc của cha nếu cần

    const baseTitle = await super.getTitle();

    console.log("Đã lấy title từ BasePage, giờ xử lý thêm ở LoginPage.");

    return `Login Page | ${baseTitle}`;

  }

}

 

"Ký Hợp Đồng" với implements - Mối quan hệ "CÓ KHẢ NĂNG" (CAN-DO)

Nếu extends là về việc thừa hưởng mã nguồn, thì implements là về việc tuân thủ một cấu trúc. Nó buộc một class phải thực thi (implement) tất cả các phương thức và thuộc tính được định nghĩa trong một interface.

Mối quan hệ cốt lõi: Hãy nghĩ về implements như mối quan hệ "CAN-DO" (Có khả năng làm gì đó). Một Bird CÓ KHẢ NĂNG fly(). Một Plane CÓ KHẢ NĂNG fly(). Chúng không liên quan về mặt kế thừa (máy bay không phải là một loài chim), nhưng chúng cùng chia sẻ một khả năng.

Cú pháp và Đặc điểm

Một interface chỉ định nghĩa "hình dáng" (tên phương thức, tham số, kiểu trả về) chứ không chứa code xử lý bên trong.

Một class có thể implements nhiều interface cùng lúc, điều mà extends không làm được (TypeScript chỉ cho phép đơn kế thừa).

Nếu một class implements một interface, nó bắt buộc phải định nghĩa tất cả các phương thức trong interface đó, nếu không TypeScript sẽ báo lỗi.

🧠 Ví von sâu hơn: interface giống như một bộ tiêu chuẩn chất lượng (ISO). Bất kỳ class (công ty) nào muốn được chứng nhận (implements) đều phải đáp ứng đầy đủ các tiêu chí (phương thức) trong bộ tiêu chuẩn đó. Việc này đảm bảo tính nhất quán và chất lượng trên diện rộng.

Ứng dụng trong Automation: Đảm bảo tính nhất quán cho Page Objects

implements là công cụ hoàn hảo để đảm bảo mọi Page Object trong dự án của bạn đều tuân theo một cấu trúc chuẩn.

 

// Định nghĩa một "hợp đồng" chung cho tất cả các page

interface IVerifiablePage {

  // Mọi trang đều phải có cách để xác thực nó đã hiển thị đúng

  verifyPageIsVisible(): Promise<void>;

  // Mọi trang đều phải có một phương thức để lấy phần tử định danh duy nhất của nó

  getUniqueElement(): any;

}

// Định nghĩa một hợp đồng khác cho các trang có form

interface IFormPage {

  submitForm(): Promise<void>;

}

// LoginPage tuân thủ cả hai "hợp đồng"

class LoginPage implements IVerifiablePage, IFormPage {

  private readonly page: any;

  private readonly loginButton: any;

  constructor(page: any) {

    this.page = page;

    this.loginButton = page.locator('button[type="submit"]');

  }

  // Bắt buộc phải có vì implements IVerifiablePage

  async verifyPageIsVisible(): Promise<void> {

    console.log("Đang xác thực nút Login có hiển thị...");

    // await expect(this.loginButton).toBeVisible();

  }

  // Bắt buộc phải có vì implements IVerifiablePage

  getUniqueElement(): any {

    return this.loginButton;

  }

  // Bắt buộc phải có vì implements IFormPage

  async submitForm(): Promise<void> {

    await this.loginButton.click();

  }

}

// HomePage chỉ cần tuân thủ một "hợp đồng"

class HomePage implements IVerifiablePage {

  private readonly page: any;

  private readonly welcomeBanner: any;

  constructor(page: any) {

    this.page = page;

    this.welcomeBanner = page.locator('.welcome-banner');

  }

  // Bắt buộc phải có vì implements IVerifiablePage

  async verifyPageIsVisible(): Promise<void> {

    console.log("Đang xác thực banner chào mừng có hiển thị...");

    // await expect(this.welcomeBanner).toBeVisible();

  }
  // Bắt buộc phải có vì implements IVerifiablePage

  getUniqueElement(): any {

    return this.welcomeBanner;

  }

}

 

So sánh và Lựa chọn: extends vs. implements

Tiêu chí

extends (Kế thừa)

implements (Tuân thủ)

Bản chất

Thừa hưởng mã nguồn và hành vi từ lớp cha.

Tuân thủ một "hợp đồng" về cấu trúc, phải tự viết mã nguồn.

Mối quan hệ

IS-A (Là một). HomePage là một BasePage.

CAN-DO (Có khả năng). LoginPage có khả năng được xác thực.

Số lượng

Chỉ có thể kế thừa từ một lớp cha duy nhất.

Có thể tuân thủ nhiều interface cùng lúc.

Mục đích

Tái sử dụng code, tạo hệ thống phân cấp.

Đảm bảo tính nhất quán, định nghĩa API chung cho các class không liên quan.


Khi nào dùng cái nào?

Dùng extends khi: Bạn muốn chia sẻ code giữa các class có mối quan hệ cha-con rõ ràng. Ví dụ: BasePage chứa các hàm dùng chung như click(), fill() và các trang con kế thừa chúng.

Dùng implements khi: Bạn muốn đảm bảo một nhóm các class (dù không liên quan) đều có chung một bộ phương thức. Ví dụ: Bắt buộc mọi Page Object phải có hàm verifyPageIsVisible().

Kết hợp cả hai: Đây là mô hình mạnh mẽ nhất! Một class có thể extends một BasePage để lấy code dùng chung, và đồng thời implements một hoặc nhiều interface để đảm bảo nó có đủ các phương thức cần thiết.

 

// Kết hợp cả hai để có được sức mạnh tối đa

class DashboardPage extends BasePage implements IVerifiablePage {

    constructor(page: any) {

        super(page); // Kế thừa các phương thức từ BasePage

    }

    // Bắt buộc phải định nghĩa vì implements IVerifiablePage

    async verifyPageIsVisible(): Promise<void> {

        // ... logic xác thực

    }


    getUniqueElement(): any {

        // ... trả về element duy nhất

    }

}


Tổng kết

extends là về việc tái sử dụng code thông qua mối quan hệ cha-con.

implements là về việc thiết lập tiêu chuẩn và đảm bảo tính nhất quán về cấu trúc.

Hiểu rõ và vận dụng linh hoạt cả hai khái niệm này sẽ giúp bạn xây dựng được những framework automation không chỉ hoạt động tốt mà còn cực kỳ dễ đọc, dễ bảo trì và dễ mở rộng trong tương lai.


PHẦN 2: LÀM CHỦ HẰNG SỐ VỚI
ENUM TRONG TYPESCRIPT

Trong bất kỳ dự án nào, chúng ta đều phải làm việc với các tập hợp giá trị cố định: vai trò người dùng (Admin, Editor), trạng thái (Pending, Success, Failed), các môi trường (Staging, Production), v.v. Việc quản lý chúng bằng các chuỗi hoặc số rời rạc sẽ sớm dẫn đến một cơn ác mộng.


Vấn đề Cốt lõi: "Magic Strings" và "Magic Numbers"

Các giá trị "ma thuật" là những chuỗi hoặc số được sử dụng trực tiếp trong code mà không có giải thích.

function setStatus(status: number) {

  // status 1 là gì? 2 là gì?

  if (status === 1) { /* ... */ }

}

// Lỗi gõ nhầm mà không ai hay biết, chương trình vẫn chạy nhưng sai logic!

checkPermissions("admon");

Hậu quả:

Dễ gõ sai: Trình biên dịch không thể phát hiện lỗi sai chính tả trong một chuỗi.

Khó đọc, khó hiểu: Người đọc code phải đoán ý nghĩa của 1 hay "ADMIN".

Khó bảo trì: Nếu muốn đổi giá trị "ADMIN" thành "SUPER_ADMIN", bạn phải tìm và thay thế ở khắp mọi nơi, rất rủi ro.

Enum (Enumeration - tập hợp liệt kê) trong TypeScript được sinh ra để tiêu diệt hoàn toàn vấn đề này, biến code của bạn trở nên an toàn, dễ đọc và tự-chú-thích (self-documenting).
Ví von: 🧠 Enum giống như việc bạn tạo ra một danh bạ điện thoại cho các hằng số. Thay vì phải nhớ số điện thoại (giá trị 1, "ADMIN"), bạn chỉ cần nhớ tên người đó (khóa UserRole.ADMIN), và TypeScript sẽ tự động tra cứu "số điện thoại" chính xác cho bạn.

Các "Hương vị" của Enum

TypeScript cung cấp nhiều loại Enum khác nhau, mỗi loại có ưu và nhược điểm riêng.


Numeric Enums (Enum Số)

Đây là loại mặc định. Các thành viên sẽ được tự động gán giá trị số, bắt đầu từ 0 và tự tăng lên.

enum OrderStatus {

  PENDING,    // 0

  PROCESSING, // 1

  SHIPPED,    // 2

  DELIVERED,  // 3

  CANCELLED   // 4

}

Bạn cũng có thể gán giá trị khởi đầu một cách tường minh:

enum ErrorCode {

  NOT_FOUND = 404,

  INTERNAL_SERVER_ERROR = 500,

  BAD_REQUEST = 400

}


Điểm đặc biệt (và cần lưu ý):
Numeric enums hỗ trợ ánh xạ ngược (reverse mapping). Bạn có thể truy cập tên của thành viên từ giá trị của nó.

console.log(OrderStatus.PROCESSING); // In ra: 1

console.log(OrderStatus[1]);         // In ra: "PROCESSING"


String Enums (Enum Chuỗi)

Đây là lựa chọn được khuyến khích sử dụng trong hầu hết các trường hợp vì tính rõ ràng của nó.

enum Environment {

  STAGING = "https://staging.my-app.com",

  PRODUCTION = "https://my-app.com",

  UAT = "https://uat.my-app.com"

}

 

Lợi ích vượt trội:

Dễ Debug: Khi bạn console.log một biến enum chuỗi, giá trị in ra ("https://staging.my-app.com") có ý nghĩa ngay lập tức, không giống như số 1 bí ẩn.

Dễ đọc trong Logs và JSON: Giá trị chuỗi có ý nghĩa làm cho dữ liệu được lưu trữ hoặc truyền đi dễ hiểu hơn nhiều.

Không có ánh xạ ngược: Điều này làm cho object được biên dịch ra JavaScript đơn giản hơn.


Const Enums (Enum Hằng số)

Đây là một phiên bản tối ưu hóa hiệu năng. const enum sẽ bị xóa hoàn toàn sau khi biên dịch, và các giá trị của nó sẽ được chèn trực tiếp (inlined) vào nơi sử dụng.

 

// Trong TypeScript

const enum LogLevel {

  INFO,

  WARN,

  ERROR

}


const level = LogLevel.ERROR;

JavaScript

// Sau khi biên dịch ra JavaScript

// const enum LogLevel biến mất!

const level = 2; // Giá trị được chèn trực tiếp

Ưu điểm: Giảm kích thước file JavaScript cuối cùng, code chạy nhanh hơn một chút vì không cần tra cứu object.

Nhược điểm: Bạn không thể truy cập LogLevel như một object lúc runtime (ví dụ: không thể lặp qua nó). Chỉ sử dụng khi bạn chắc chắn chỉ cần giá trị của enum tại thời điểm biên dịch.

Ứng dụng Nâng cao trong Automation

Quản lý Cấu hình và Trạng thái một cách an toàn

Enum giúp định nghĩa một tập hợp các lựa chọn hữu hạn, giúp các hàm trở nên an toàn hơn.

enum Browser {

  CHROME = "chrome",

  FIREFOX = "firefox",

  WEBKIT = "webkit" // Safari

}


enum TestStatus {

    PASSED = "passed",

    FAILED = "failed",

    SKIPPED = "skipped"

}

// Sử dụng Enum làm kiểu dữ liệu trong một interface

interface TestResult {

    testName: string;

    duration: number;

    status: TestStatus; // Chỉ chấp nhận các giá trị từ TestStatus enum

}

// Hàm này giờ đây cực kỳ an toàn

function runTestOnBrowser(browser: Browser): TestResult {

  console.log(`Đang chạy test trên ${browser}...`);

  // ... logic test

  // Giả sử test thành công

  return {

      testName: "Login Test",

      duration: 1500,

      status: TestStatus.PASSED // Không thể gõ nhầm "pass" hay "Passed"

  };

}
// Gọi hàm một cách an toàn

runTestOnBrowser(Browser.CHROME);

// Dòng này sẽ báo lỗi ngay lập tức!

// runTestOnBrowser("edge"); // LỖI: Argument of type '"edge"' is not assignable to parameter of type 'Browser'.

Lặp qua các thành viên của Enum

Đôi khi bạn cần lấy tất cả các tùy chọn có thể có từ một enum, ví dụ để hiển thị trong một danh sách dropdown hoặc để kiểm tra một giá trị đầu vào.

Lưu ý: Kỹ thuật này không hoạt động với const enum.

enum UserRole {

  ADMIN = "ADMIN",

  EDITOR = "EDITOR",

  VIEWER = "VIEWER"

}
// Lấy danh sách các vai trò (dạng chuỗi)

const allRoles = Object.values(UserRole);

console.log(allRoles); // ["ADMIN", "EDITOR", "VIEWER"]

// Dùng trong automation để test một form với tất cả các tùy chọn

for (const role of allRoles) {

    // selectDropdownOption('roleSelector', role);

    // submitForm();

    // verifyResultForRole(role);

    console.log(`Đã test với vai trò: ${role}`);

}


Kết luận và "Best Practices"

Enum là một tính năng tuy nhỏ nhưng có tác động rất lớn đến chất lượng mã nguồn.

Ưu tiên sử dụng String Enums: Chúng giúp việc debug và đọc log dễ dàng hơn rất nhiều.

Sử dụng PascalCase cho tên Enum và UPPER_CASE cho các thành viên (OrderStatus, TestStatus.PASSED). Đây là một quy ước phổ biến giúp code dễ đọc.

Sử dụng const enum trong các tình huống yêu cầu hiệu năng cao và bạn không cần truy cập enum như một object lúc runtime.

Tránh Heterogeneous Enums (trộn lẫn số và chuỗi trong cùng một enum) vì chúng có thể gây nhầm lẫn.

Tận dụng Enum như một kiểu dữ liệu cho các tham số hàm và thuộc tính của object/interface để tận dụng tối đa khả năng kiểm tra kiểu của TypeScript.

Bằng cách thay thế "magic strings/numbers" bằng các Enum được định nghĩa rõ ràng, bạn đang làm cho code của mình không chỉ chạy đúng mà còn trở nên mạnh mẽ, dễ bảo trì và dễ hiểu hơn cho chính bạn và đồng đội trong tương lai.

 

PHẦN 3: GENERICS - NGHỆ THUẬT TẠO RA CÁC THÀNH PHẦN "ĐA NĂNG"

Generics là một trong những tính năng cốt lõi và mạnh mẽ nhất của TypeScript. Ban đầu, khái niệm này có thể hơi trừu tượng, nhưng một khi bạn nắm vững, nó sẽ thay đổi hoàn toàn cách bạn viết code: linh hoạt, an toàn và có khả năng tái sử dụng tối đa.


Vấn đề Nguồn gốc: Sự Lặp lại Code vì Kiểu dữ liệu

Hãy xem xét một nhu cầu rất cơ bản: viết một hàm để xử lý dữ liệu.

 

// Hàm đảo ngược mảng SỐ

function reverseNumbers(items: number[]): number[] {

  return items.reverse();

}

// Hàm đảo ngược mảng CHUỖI

function reverseStrings(items: string[]): string[] {

  return items.reverse();

}

// Hàm đảo ngược mảng ĐỐI TƯỢNG User

function reverseUsers(items: User[]): User[] {

  return items.reverse();

}

Chúng ta thấy rõ một vấn đề: logic bên trong các hàm này giống hệt nhau 100%. Chỉ có kiểu dữ liệu (number, string, User) là khác biệt. Việc viết một hàm cho mỗi kiểu dữ liệu là một sự lãng phí, vi phạm nguyên tắc DRY (Don't Repeat Yourself).

Giải pháp "tồi" là dùng any, nhưng nó phá vỡ toàn bộ giá trị mà TypeScript mang lại.

 

function reverseAnything(items: any[]): any[] {

  return items.reverse();

}


const reversedNames = reverseAnything(["Alice", "Bob"]);

// Kiểu dữ liệu của reversedNames là any[].

// TypeScript không biết phần tử bên trong là string.

// Sẽ không có gợi ý code, không có sự an toàn về kiểu.

// reversedNames[0].toUpperCase(); // Không có gợi ý, có thể gây lỗi lúc runtime.

Generics ra đời để giải quyết triệt để vấn đề này: viết code một lần, hoạt động an toàn với nhiều kiểu dữ liệu.


Generics là gì? - "Kiểu" của các Kiểu

Generics cho phép bạn định nghĩa các hàm, class, và interface với các tham số kiểu (type parameters). Các tham số này hoạt động như những biến giữ chỗ (placeholder) cho các kiểu dữ liệu cụ thể sẽ được cung cấp sau này.

🧠 Ví von sâu hơn: Hãy tưởng tượng Generics là một bản thiết kế cho một "Robot Đa năng".

Bạn không thiết kế "Robot gắp Táo" hay "Robot gắp Cam".

Bạn thiết kế một "Robot Gắp Vật Thể" (Robot<T>). Bản thiết kế này có ghi rõ: "Tôi sẽ gắp một vật thể loại T và đặt nó vào giỏ loại T".

Khi bạn ra lệnh: "Hãy dùng bản thiết kế này cho Táo", TypeScript tạo ra một robot chuyên gắp Táo. Nó biết đầu vào là Táo và đầu ra cũng là Táo.

Khi bạn ra lệnh: "Hãy dùng bản thiết kế này cho Cam", TypeScript tạo ra một robot chuyên gắp Cam.

Cái "nhãn dán tạm thời" hay "tham số kiểu" đó trong Generics thường được ký hiệu là <T> (viết tắt của Type), nhưng bạn có thể dùng bất kỳ chữ cái nào khác (<U>, <K>, <V>).


Cú pháp và Các Ứng dụng Cơ bản


Generic Functions (Hàm Generic)

Bạn khai báo một hoặc nhiều tham số kiểu trong dấu <> ngay sau tên hàm.

 

// <T> là tham số kiểu. Nó đại diện cho một kiểu dữ liệu bất kỳ.

function getFirst<T>(items: T[]): T | undefined {

  return items[0];

}

// --- Cách sử dụng ---

const stringArray = ["Chrome", "Firefox", "Safari"];

const numberArray = [100, 200, 300];



// Cách 1: TypeScript tự suy luận kiểu (Type Inference) - Phổ biến nhất

const firstString = getFirst(stringArray); // TS tự hiểu T là 'string'

if (firstString) {

  console.log(firstString.toUpperCase()); // Gợi ý code hoạt động hoàn hảo!

}

const firstNumber = getFirst(numberArray); // TS tự hiểu T là 'number'

if (firstNumber) {

  console.log(firstNumber.toFixed(2)); // Gợi ý code hoạt động hoàn hảo!

}

// Cách 2: Chỉ định kiểu một cách tường minh

const firstBoolean = getFirst<boolean>([true, false]);


Generic Interfaces & Classes

Generics đặc biệt hữu dụng khi định nghĩa các cấu trúc dữ liệu "bao bọc" các loại dữ liệu khác.

Ví dụ thực tế trong Automation 🧪: Khi test API, một response trả về thường có cấu trúc chung, nhưng phần data cốt lõi thì thay đổi.

 

// Tạo một interface generic cho mọi loại API response

interface ApiResponse<DataType> {

  success: boolean;

  statusCode: number;

  data: DataType; // 'data' sẽ có kiểu là DataType, do người dùng quyết định

  error?: string;

}



// Định nghĩa các kiểu dữ liệu cụ thể

type User = { id: number; name: string; email: string };

type Product = { sku: string; price: number; stock: number };




// Giờ đây, chúng ta có thể tạo ra các kiểu response cụ thể một cách an toàn

const userResponse: ApiResponse<User> = {

  success: true,

  statusCode: 200,

  data: { id: 123, name: "Automation Tester", email: "test@example.com" }

};




// Data có thể là một MẢNG các đối tượng

const productListResponse: ApiResponse<Product[]> = {

  success: true,

  statusCode: 200,

  data: [

    { sku: "TS-001", price: 25.99, stock: 100 },

    { sku: "TS-002", price: 29.99, stock: 50 }

  ]

};


// Nếu API trả về lỗi

const errorResponse: ApiResponse<null> = {

  success: false,

  statusCode: 404,

  data: null,

  error: "Resource not found"

};

 

Ví dụ với Generic Class:

 

class DataStore<T> {

  private data: T[] = [];

  addItem(item: T): void {

    this.data.push(item);

  }

  getItemByIndex(index: number): T | undefined {

    return this.data[index];

  }

}


// Tạo một store chỉ dành cho 'string'

const stringStore = new DataStore<string>();

stringStore.addItem("Hello");

// stringStore.addItem(123); // LỖI! Chỉ chấp nhận string


// Tạo một store chỉ dành cho 'User'

const userStore = new DataStore<User>();

userStore.addItem({ id: 1, name: "User A", email: "a@a.com" });


Ràng buộc Generic (Generic Constraints)

Điều gì sẽ xảy ra nếu hàm generic của bạn cần một thuộc tính hoặc phương thức nào đó trên tham số đầu vào?

 

function logLength<T>(arg: T) {

  // LỖI: Property 'length' does not exist on type 'T'.

  // TypeScript không biết liệu kiểu 'T' có thuộc tính .length hay không.

  // console.log(arg.length);

}

Đây là lúc Ràng buộc (Constraints) phát huy tác dụng. Ta dùng từ khóa extends để yêu cầu T phải tuân thủ theo một "hình dáng" (shape) nhất định.

// Chúng ta ràng buộc T phải là một kiểu có thuộc tính 'length' kiểu 'number'

interface WithLength {

  length: number;

}


function logLength<T extends WithLength>(arg: T): void {

  // Giờ thì an toàn rồi!

  console.log(`Độ dài là: ${arg.length}`);

}


logLength("Hello TypeScript"); // OK, string có .length

logLength([1, 2, 3, 4]);     // OK, array có .length

// logLength(123);              // LỖI! number không có .length

// logLength({ name: "Test" }); // LỖI! object này không có .length

 

Ràng buộc với keyof

Một ràng buộc cực kỳ mạnh mẽ khác là keyof. Nó cho phép bạn đảm bảo rằng một tham số là một key hợp lệ của một object.

// K phải là một key của T

function getProperty<T, K extends keyof T>(obj: T, key: K) {

  return obj[key];

}

const user = { name: "Tester", age: 30 };

const userName = getProperty(user, "name"); // OK, 'name' là key của user

const userAge = getProperty(user, "age");   // OK, 'age' là key của user

// const userLocation = getProperty(user, "location"); // LỖI! 'location' không phải là key của user.

 

Tổng kết

Generics là một công cụ nền tảng để viết code TypeScript ở đẳng cấp cao.

Mục đích: Tái sử dụng code mà không phải hy sinh sự an toàn về kiểu.

Khi nào dùng:

Khi bạn thấy mình đang viết cùng một logic cho nhiều kiểu dữ liệu khác nhau.

Khi bạn muốn tạo một cấu trúc dữ liệu (như ApiResponse, DataStore) có thể chứa hoặc làm việc với nhiều loại dữ liệu khác nhau một cách linh hoạt.

Khi bạn cần viết các hàm tiện ích (utility functions) có thể hoạt động trên một loạt các kiểu nhưng cần đảm bảo chúng có một số thuộc tính/phương thức nhất định (sử dụng constraints).

Nắm vững Generics sẽ giúp bạn xây dựng các hàm, component, và các framework automation vừa linh hoạt, vừa mạnh mẽ, lại cực kỳ an toàn và dễ bảo trì.

 

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