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: extends, super() và kế thừa class trong TypeScript

 

Trong OOP, một trong những cách mạnh nhất để tránh lặp code là kế thừa. TypeScript dùng từ khóa extends để cho phép class con nhận lại thuộc tính và phương thức từ class cha.

Hãy xem đây là quan hệ “IS-A”: HomePage là một BasePage, LoginPage cũng là một BasePage, nhưng mỗi trang lại có thêm hành vi riêng.

 

🔹 1. Vai trò của extendssuper()

 

extends tạo quan hệ cha-con giữa hai class. Còn super() dùng trong constructor của class con để gọi constructor của class cha.

Bạn phải gọi super() trước khi dùng this trong constructor của class con. Lý do rất đơn giản: class cha cần được khởi tạo trước thì class con mới có nền móng để dùng tiếp.

 

🔹 2. Dùng abstract BasePage để gom logic chung

 

Trong automation, đây là pattern rất quen thuộc. Thay vì lặp lại goto(), title(), screenshot() ở mọi page object, bạn gom chúng vào một class cha.

 

abstract class BasePage {
  protected readonly page: any; // Chỉ lớp con mới nên truy cập trực tiếp

  constructor(page: any) {
    this.page = page;
  }

  async navigateTo(url: string): Promise<void> {
    await this.page.goto(url); // Hành động dùng chung cho mọi trang
  }

  async getTitle(): Promise<string> {
    return await this.page.title(); // Lấy title của trang hiện tại
  }

  async takeScreenshot(fileName: string): Promise<void> {
    await this.page.screenshot({ path: `screenshots/${fileName}.png` });
  }
}

Từ khóa abstract cho biết đây là class nền tảng, dùng để kế thừa chứ không nhằm tạo object trực tiếp.

 

🔹 3. Class con kế thừa và thêm hành vi riêng

 

class HomePage extends BasePage {
  public welcomeMessage: any;

  constructor(page: any) {
    super(page); // Bắt buộc gọi trước khi dùng this
    this.welcomeMessage = page.locator(".welcome-message");
  }

  async getWelcomeText(): Promise<string> {
    return await this.welcomeMessage.textContent();
  }
}

Nhờ kế thừa, HomePage không cần viết lại navigateTo(), getTitle() hay takeScreenshot(), mà vẫn có thể thêm những locator và method riêng của trang chủ.

 

🔹 4. Ghi đè phương thức với method overriding

 

Kế thừa không có nghĩa là class con phải dùng y nguyên hành vi của class cha. Khi cần, bạn có thể định nghĩa lại cùng tên method để thay đổi hành vi, và thậm chí gọi lại logic gốc bằng super.methodName().

 

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);
  }

  async getTitle(): Promise<string> {
    const baseTitle = await super.getTitle(); // Gọi lại logic gốc từ BasePage
    return `Login Page | ${baseTitle}`; // Xử lý thêm theo nhu cầu riêng
  }
}

 

💡 Mẹo nhớ nhanh: extends là để tái sử dụng code theo quan hệ cha-con. super() là bước khởi tạo nền móng chung của class cha trước khi class con dùng tiếp.

 

📋 Phần 2: implements - “ký hợp đồng” cho class và kết hợp với extends

 

Nếu extends là chuyện thừa hưởng code, thì implements là chuyện tuân thủ cấu trúc. Một class khi implements interface nào đó thì bắt buộc phải cài đặt đầy đủ các method và property được yêu cầu.

Đây là quan hệ “CAN-DO”: class đó có khả năng làm gì, chứ không nhất thiết là con của một class cụ thể.

 

🔹 1. Một class có thể implements nhiều interface

 

Đây là điểm rất đáng giá, vì TypeScript chỉ cho một class extends một class cha, nhưng lại cho implements nhiều interface cùng lúc.

 

interface IVerifiablePage {
  verifyPageIsVisible(): Promise<void>;
  getUniqueElement(): any;
}

interface IFormPage {
  submitForm(): Promise<void>;
}

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"]');
  }

  async verifyPageIsVisible(): Promise<void> {
    console.log("Đang xác thực trang login hiển thị đúng...");
  }

  getUniqueElement(): any {
    return this.loginButton;
  }

  async submitForm(): Promise<void> {
    await this.loginButton.click();
  }
}

Nếu thiếu một trong ba method ở trên, TypeScript sẽ báo lỗi ngay vì class chưa hoàn thành “hợp đồng”.

 

🔹 2. Ví dụ với page khác chỉ cần 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");
  }

  async verifyPageIsVisible(): Promise<void> {
    console.log("Đang xác thực banner chào mừng hiển thị...");
  }

  getUniqueElement(): any {
    return this.welcomeBanner;
  }
}

 

🔹 3. Kết hợp extendsimplements

 

Đây là mô hình rất mạnh trong framework automation: extends để lấy code dùng chung từ BasePage, và implements để buộc page object phải có cấu trúc chuẩn.

 

class DashboardPage extends BasePage implements IVerifiablePage {
  constructor(page: any) {
    super(page); // Lấy lại toàn bộ logic dùng chung từ BasePage
  }

  async verifyPageIsVisible(): Promise<void> {
    console.log("Đang xác thực dashboard hiển thị...");
  }

  getUniqueElement(): any {
    return this.page.locator(".dashboard-title");
  }
}

 

🔹 4. Chọn cái nào trong thực tế?

 

  • Dùng extends: khi muốn chia sẻ code giữa các class cha-con rõ ràng.
  • Dùng implements: khi muốn đảm bảo nhiều class khác nhau đều tuân cùng một API.
  • Dùng cả hai: khi muốn vừa tái sử dụng code, vừa ép cấu trúc thống nhất cho framework.

 

🧭 Phần 3: enum - quản lý hằng số an toàn và dễ đọc

 

Trong dự án thực tế, bạn luôn có các nhóm giá trị cố định như trạng thái test, vai trò user, môi trường chạy hoặc trình duyệt. Nếu cứ dùng string và number rải rác trong code, bạn sẽ rơi vào tình trạng magic stringsmagic numbers.

enum ra đời để gom các giá trị cố định đó thành một tập rõ ràng, dễ đọc và dễ kiểm soát hơn.

 

🔹 1. Vì sao nên tránh magic strings và magic numbers?

 

function setStatus(status: number): void {
  if (status === 1) {
    console.log("Đang xử lý...");
  }
}

function checkPermissions(role: string): void {
  if (role === "admin") {
    console.log("Có quyền quản trị.");
  }
}

checkPermissions("admon"); // Gõ sai nhưng rất khó phát hiện nếu không kiểm tra kỹ

Vấn đề của kiểu code này là khó đọc, dễ gõ sai, khó thay đổi đồng loạt và rất dễ gây lỗi logic âm thầm.

 

🔹 2. Numeric enum và String enum

 

Numeric enum là loại mặc định, các giá trị tự tăng từ 0 nếu bạn không gán tay.

 

enum OrderStatus {
  PENDING, // 0
  PROCESSING, // 1
  SHIPPED, // 2
  DELIVERED, // 3
  CANCELLED // 4
}

enum ErrorCode {
  NOT_FOUND = 404,
  INTERNAL_SERVER_ERROR = 500,
  BAD_REQUEST = 400
}

String enum thường dễ đọc hơn trong log, JSON và lúc debug nên thường được ưu tiên hơn trong automation.

 

enum Environment {
  STAGING = "https://staging.my-app.com",
  PRODUCTION = "https://my-app.com",
  UAT = "https://uat.my-app.com"
}

 

🔹 3. Một điểm đặc biệt của numeric enum: reverse mapping

 

enum ProcessStatus {
  NEW,
  RUNNING,
  DONE
}

console.log(ProcessStatus.RUNNING); // 1
console.log(ProcessStatus[1]); // "RUNNING"

Tính năng này không có ở string enum, nhưng string enum lại dễ đọc hơn khi log ra màn hình.

 

🔹 4. const enum để tối ưu hơn khi cần

 

const enum sẽ bị inline khi biên dịch, tức là giá trị được chèn trực tiếp vào code JavaScript sinh ra. Nó gọn hơn một chút nhưng không dùng được như object lúc runtime.

 

const enum LogLevel {
  INFO,
  WARN,
  ERROR
}

const level = LogLevel.ERROR; // Sau khi biên dịch có thể chỉ còn giá trị số tương ứng

 

🔹 5. Ứng dụng enum trong automation

 

enum Browser {
  CHROME = "chrome",
  FIREFOX = "firefox",
  WEBKIT = "webkit"
}

enum TestStatus {
  PASSED = "passed",
  FAILED = "failed",
  SKIPPED = "skipped"
}

interface TestResult {
  testName: string;
  duration: number;
  status: TestStatus;
}

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

  return {
    testName: "Login Test",
    duration: 1500,
    status: TestStatus.PASSED
  };
}

runTestOnBrowser(Browser.CHROME);
// runTestOnBrowser("edge"); // Lỗi: chỉ chấp nhận Browser enum

 

🔹 6. Lặp qua toàn bộ giá trị của enum

 

enum UserRole {
  ADMIN = "ADMIN",
  EDITOR = "EDITOR",
  VIEWER = "VIEWER"
}

const allRoles = Object.values(UserRole);

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

 

⚠️ Lưu ý: Với automation, hãy ưu tiên String Enums vì chúng dễ debug hơn, dễ đọc hơn trong log và JSON hơn so với numeric enum.

 

🧠 Phần 4: Generics - viết một lần nhưng dùng an toàn cho nhiều kiểu dữ liệu

 

Generics là một trong những tính năng mạnh nhất của TypeScript. Ý tưởng cốt lõi là: viết một logic chung, nhưng vẫn giữ được kiểu dữ liệu cụ thể cho từng lần sử dụng.

 

🔹 1. Vấn đề gốc: lặp code vì khác kiểu dữ liệu

 

function reverseNumbers(items: number[]): number[] {
  return items.reverse();
}

function reverseStrings(items: string[]): string[] {
  return items.reverse();
}

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

function reverseUsers(items: User[]): User[] {
  return items.reverse();
}

Ba hàm trên có logic giống hệt nhau, chỉ khác kiểu dữ liệu. Đây là điểm mà generics giải quyết rất đẹp.

 

🔹 2. Vì sao any không phải lời giải tốt?

 

function reverseAnything(items: any[]): any[] {
  return items.reverse();
}

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

// reversedNames là any[]
// TypeScript không biết phần tử bên trong là string
// reversedNames[0].toUpperCase(); // Không còn an toàn kiểu dữ liệu

any giải quyết được chuyện tái sử dụng, nhưng lại phá luôn lợi ích lớn nhất của TypeScript: kiểm tra kiểu và gợi ý code chính xác.

 

🔹 3. Generic function cơ bản

 

Generics dùng một tham số kiểu như <T> để giữ chỗ cho kiểu dữ liệu thực tế sẽ được truyền vào sau.

 

function getFirst<T>(items: T[]): T | undefined {
  return items[0];
}

const firstString = getFirst(["Chrome", "Firefox", "Safari"]);
const firstNumber = getFirst([100, 200, 300]);
const firstBoolean = getFirst<boolean>([true, false]);

if (firstString) {
  console.log(firstString.toUpperCase()); // TypeScript biết đây là string
}

if (firstNumber) {
  console.log(firstNumber.toFixed(2)); // TypeScript biết đây là number
}

Đây chính là điểm mạnh của generics: cùng một hàm, nhưng mỗi lần gọi lại giữ được kiểu cụ thể tương ứng.

 

🔹 4. Generic interface cho API response

 

Automation API rất hay gặp kiểu dữ liệu có “vỏ chung” nhưng phần data thay đổi theo từng endpoint. Generic interface xử lý tình huống này cực đẹp.

 

interface ApiResponse<DataType> {
  success: boolean;
  statusCode: number;
  data: DataType;
  error?: string;
}

type UserInfo = { id: number; name: string; email: string };
type Product = { sku: string; price: number; stock: number };

const userResponse: ApiResponse<UserInfo> = {
  success: true,
  statusCode: 200,
  data: {
    id: 123,
    name: "Automation Tester",
    email: "test@example.com"
  }
};

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 }
  ]
};

 

🔹 5. Generic class để tạo thành phần dùng lại

 

class DataStore<T> {
  private data: T[] = [];

  addItem(item: T): void {
    this.data.push(item);
  }

  getItemByIndex(index: number): T | undefined {
    return this.data[index];
  }
}

const stringStore = new DataStore<string>();
stringStore.addItem("Hello");
// stringStore.addItem(123); // Lỗi: store này chỉ nhận string

const userStore = new DataStore<User>();
userStore.addItem({ id: 1, name: "User A" });

Pattern này rất hữu ích khi bạn muốn tạo cache, store, wrapper hoặc repository có thể tái dùng cho nhiều kiểu dữ liệu khác nhau.

 

🔐 Phần 5: Generic Constraints và keyof - linh hoạt nhưng vẫn có giới hạn an toàn

 

Generics rất mạnh, nhưng đôi khi bạn không muốn nó “tự do tuyệt đối”. Có những lúc bạn cần ép kiểu generic phải có một số thuộc tính nhất định. Đó là lúc constraints xuất hiện.

 

🔹 1. Khi generic quá tự do sẽ gặp vấn đề gì?

 

function logLength<T>(arg: T): void {
  // console.log(arg.length);
  // Lỗi: TypeScript không biết T có thuộc tính length hay không
}

Nếu bạn chưa ràng buộc gì cho T, TypeScript không có lý do gì để tin rằng mọi kiểu dữ liệu đều có length.

 

🔹 2. Ràng buộc generic bằng extends

 

interface WithLength {
  length: number;
}

function logLength<T extends WithLength>(arg: T): void {
  console.log(`Độ dài là: ${arg.length}`);
}

logLength("Hello TypeScript"); // OK vì string có length
logLength([1, 2, 3, 4]); // OK vì array có length

// logLength(123); // Lỗi vì number không có length

Ở đây, T extends WithLength nghĩa là: kiểu nào muốn đi vào hàm này thì phải có ít nhất thuộc tính length.

 

🔹 3. Ràng buộc với keyof để truy cập property an toàn

 

Đây là một pattern cực kỳ mạnh trong TypeScript. Nó đảm bảo rằng key truyền vào thật sự tồn tại trong object.

 

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
const userAge = getProperty(user, "age"); // OK

// const userLocation = getProperty(user, "location");
// Lỗi vì "location" không phải key của user

Kiểu ràng buộc này rất hữu ích khi viết utility để đọc config, parse API data, map field hoặc xây helper thao tác object trong framework.

 

🔹 Ghi nhớ nhanh cho Tester

 

  • extends dùng để kế thừa code giữa class cha và class con.
  • implements dùng để buộc class tuân theo một cấu trúc chuẩn của interface.
  • abstract BasePage là nền tảng rất phổ biến để gom logic dùng chung cho framework automation.
  • enum giúp loại bỏ magic strings và magic numbers, làm code an toàn và dễ đọc hơn.
  • Trong đa số tình huống automation, String Enums là lựa chọn dễ debug nhất.
  • Generics giúp viết một logic chung nhưng vẫn giữ được kiểu dữ liệu cụ thể cho từng lần dùng.
  • Generic Constraints giúp generic đủ linh hoạt nhưng không “thả cửa” quá mức.
  • keyof rất hữu ích khi muốn truy cập property của object một cách an toàn.

 

Kết luận: Bài 11 là bước chuyển từ TypeScript “biết dùng” sang TypeScript “biết tổ chức framework”. Khi nắm chắc extends, implements, enum, genericsconstraints, bạn đã có một nền tảng rất vững để xây framework automation vừa tái sử dụng tốt, vừa an toàn và dễ mở rộng.

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