NỘI DUNG BÀI HỌC

  • 🚀 Linh hoạt & Vững chắc: Nắm vững cách dùng Type AliasInterface để định hình dữ liệu.

  • 🧩 Kiểu Đa năng: Khám phá Union Types và định nghĩa "hợp đồng" cho hàm với Function Types.

  • 🛡️ OOP An toàn: Xây dựng Class chuyên nghiệp với private, protected và các cú pháp nâng cao.



🧩 Phần 1: typeinterface - đặt tên cho kiểu dữ liệu một cách bài bản

 

Khi học TypeScript sâu hơn, bạn sẽ nhanh chóng gặp một vấn đề quen thuộc: cùng một cấu trúc dữ liệu lặp đi lặp lại ở nhiều nơi. Nếu cứ viết inline mãi, code sẽ dài, khó đọc và rất dễ sai.

Hai công cụ quan trọng để xử lý chuyện này là typeinterface. Cả hai đều giúp bạn mô tả rõ “hình dạng” của dữ liệu, nhưng mỗi cái lại phù hợp với những tình huống khác nhau.

 

🔹 1. type - linh hoạt và dùng được cho rất nhiều kiểu

 

type cho phép bạn tạo một tên mới cho một kiểu dữ liệu. Tên mới đó có thể đại diện cho object, union, tuple, primitive hoặc cả function type.

 

type User = {
  readonly id: number; // ID chỉ đọc, không nên sửa sau khi tạo
  name: string;
  role?: string; // Thuộc tính tùy chọn, có thể có hoặc không
  isActive: boolean;
};

let user1: User = {
  id: 1,
  name: "Admin",
  isActive: true
};

let user2: User = {
  id: 2,
  name: "Guest",
  role: "Viewer",
  isActive: false
};

Điểm hay của cách làm này là bạn chỉ cần định nghĩa một lần, sau đó dùng lại ở mọi nơi. Điều đó giúp code gọn và nhất quán hơn rất nhiều.

 

🔹 2. Những tình huống mà type đặc biệt mạnh

 

Rất nhiều người mới chỉ nghĩ type là “đặt tên cho object”, nhưng thực ra sức mạnh lớn của nó nằm ở chỗ nó có thể dùng cho nhiều loại kiểu dữ liệu khác nhau.

 

💻 Ví dụ với literal union:

 

type Status = "Passed" | "Failed" | "Skipped" | "Pending";

type TestResult = {
  testCaseId: string;
  status: Status; // Chỉ cho phép 1 trong 4 giá trị đã khai báo
};

const result1: TestResult = {
  testCaseId: "TC-01",
  status: "Passed"
};

// const result2: TestResult = {
//   testCaseId: "TC-02",
//   status: "Error"
// };
// Lỗi vì "Error" không nằm trong kiểu Status

 

💻 Ví dụ với primitive alias:

 

type Email = string;
type UserId = number;

function sendEmailTo(email: Email, userId: UserId): void {
  console.log(`Gửi email tới ${email} cho user ${userId}`);
}

Dù về kỹ thuật Email vẫn là stringUserId vẫn là number, cách đặt tên này giúp code có ngữ nghĩa rõ hơn nhiều.

 

💻 Ví dụ với test data:

 

type LoginTestData = {
  readonly testId: string; // Mã test case không nên đổi giữa chừng
  description: string;
  username: string;
  password: string;
  expectedMessage: string;
};

const loginScenarios: LoginTestData[] = [
  {
    testId: "TC-01",
    description: "Đăng nhập với tài khoản hợp lệ",
    username: "tomsmith",
    password: "SuperSecretPassword!",
    expectedMessage: "You logged into a secure area!"
  },
  {
    testId: "TC-02",
    description: "Đăng nhập với username sai",
    username: "wronguser",
    password: "SuperSecretPassword!",
    expectedMessage: "Your username is invalid!"
  }
];

Với automation, đây là kiểu áp dụng cực kỳ thực chiến vì dữ liệu test gần như luôn cần có format rõ ràng và ổn định.

 

🔹 3. interface - “hợp đồng” rất hợp với object và OOP

 

interface cũng dùng để mô tả cấu trúc object, nhưng nó thiên mạnh về tư duy hướng đối tượng hơn. Hãy xem nó như một “hợp đồng”: object hay class nào đã nhận hợp đồng thì phải tuân thủ đầy đủ các thuộc tính và phương thức được khai báo.

 

interface Person {
  name: string;
  age: number;
  greet(): void; // Bắt buộc phải có phương thức này
}

interface Employee extends Person {
  employeeId: string;
  department: string;
}

const emp: Employee = {
  name: "John",
  age: 30,
  employeeId: "E123",
  department: "Engineering",
  greet() {
    console.log("Xin chào, tôi là nhân viên mới.");
  }
};

Điểm nổi bật ở đây là extends rất tự nhiên. Bạn có thể xây các interface con từ interface cha mà vẫn giữ code rất rõ ràng.

 

🔹 4. Một khác biệt đáng nhớ của interface: declaration merging

 

Nếu bạn định nghĩa nhiều interface cùng tên, TypeScript có thể tự gộp chúng lại. Đây là đặc điểm khá riêng của interface, còn type thì không làm vậy.

 

interface Box {
  height: number;
  width: number;
}

interface Box {
  length: number; // TypeScript sẽ gộp thêm thuộc tính này
}

const myBox: Box = {
  height: 10,
  width: 20,
  length: 30
};

Tính năng này đặc biệt hữu ích khi làm việc với thư viện bên ngoài mà bạn muốn bổ sung thêm thuộc tính cho một interface có sẵn.

 

⚖️ Phần 2: Chọn type hay interface trong thực tế?

 

Đây là câu hỏi rất phổ biến khi mới học TypeScript. Câu trả lời thực tế là: không có một bên thắng tuyệt đối. Bạn cần chọn đúng công cụ theo ngữ cảnh.

 

🔹 1. Khi nào nên nghiêng về interface?

 

  • Khi mô tả object hoặc class: đây là vùng mạnh tự nhiên của interface.
  • Khi làm OOP:extendsimplements đọc rất tự nhiên.
  • Khi xây framework theo Page Object Model: bạn thường cần những “hợp đồng chung” cho nhiều page object.
  • Khi muốn hỗ trợ mở rộng theo declaration merging: chỉ interface làm được việc này.

 

🔹 2. Khi nào nên nghiêng về type?

 

  • Khi cần union: ví dụ string | number, literal union, status union.
  • Khi cần function type: mô tả “hình dạng” của một hàm.
  • Khi cần tuple hoặc alias cho primitive: đây là sân chơi rất tự nhiên của type.
  • Khi đang đặt tên cho dữ liệu test, trạng thái, callback, ID: type thường tiện và gọn hơn.

 

🔹 3. Ví dụ rất thực tế trong Page Object Model

 

Trong automation, bạn thường muốn mọi page object đều có một số hành vi chuẩn như đi tới trang và xác nhận đang ở đúng trang. Khi đó, interface là lựa chọn rất hợp lý.

 

interface IPageObject {
  page: any; // Tạm dùng any, sau này có thể thay bằng kiểu Page của Playwright
  goTo(): Promise<void>;
  assertIsOnPage(): Promise<void>;
}

class LoginPage implements IPageObject {
  page: any;

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

  async goTo(): Promise<void> {
    await this.page.goto("/login"); // Điều hướng tới trang login
  }

  async assertIsOnPage(): Promise<void> {
    await this.page.waitForURL("**/login"); // Xác nhận đang ở đúng URL
  }
}

Nếu class LoginPage thiếu một trong hai phương thức trên, TypeScript sẽ nhắc bạn ngay. Đây là kiểu an toàn rất đáng giá khi framework bắt đầu lớn.

 

💡 Mẹo nhớ nhanh: Nếu bạn đang nghĩ “đây là hợp đồng cho object/class”, hãy bắt đầu với interface. Nếu bạn đang nghĩ “đây là một kiểu dữ liệu linh hoạt”, hãy bắt đầu với type.

 

🔀 Phần 3: Union TypesFunction Types - tạo kiểu dữ liệu đa năng nhưng vẫn an toàn

 

TypeScript không chỉ giúp code chặt hơn, mà còn giúp code linh hoạt hơn nếu bạn dùng đúng công cụ. Hai công cụ rất mạnh trong nhóm này là Union TypeFunction Type.

 

🔹 1. Union Type - “hoặc là kiểu này, hoặc là kiểu kia”

 

Union Type cho phép một biến, tham số hoặc thuộc tính nhận nhiều kiểu giá trị khác nhau. Đây là cách rất hay để làm utility function linh hoạt mà không phải viết nhiều phiên bản hàm trùng nhau.

 

let userIdentifier: string | number;

userIdentifier = 101; // OK
userIdentifier = "auth-xyz-789"; // OK

// userIdentifier = true; // Lỗi vì boolean không nằm trong union này

 

🔹 2. Type Narrowing - thu hẹp kiểu trước khi dùng

 

Khi một biến có nhiều khả năng kiểu, TypeScript sẽ không cho bạn gọi bừa các phương thức đặc thù của một kiểu cụ thể. Bạn phải kiểm tra trước để “thu hẹp kiểu”.

 

function processIdentifier(id: string | number): void {
  // console.log(id.toUpperCase());
  // Lỗi vì id có thể là number

  if (typeof id === "string") {
    console.log("ID dạng chuỗi:", id.toUpperCase()); // OK trong nhánh string
  } else {
    console.log("ID dạng số:", id.toFixed(2)); // OK trong nhánh number
  }
}

Đây là cơ chế rất quan trọng để TypeScript vừa cho phép bạn linh hoạt, vừa không để bạn gọi nhầm phương thức gây lỗi runtime.

 

🔹 3. Union trong automation

 

Một ví dụ rất sát thực tế là hàm tìm element. Có lúc bạn muốn truyền trực tiếp selector, có lúc lại muốn truyền cả một object mô tả locator.

 

type LocatorInfo = {
  selector: string;
  description: string;
};

function findElement(locator: string | LocatorInfo): void {
  if (typeof locator === "string") {
    console.log(`Tìm phần tử bằng selector: ${locator}`);
  } else {
    console.log(`Tìm phần tử: ${locator.description}`);
    console.log(`Selector tương ứng: ${locator.selector}`);
  }
}

findElement("#username");

findElement({
  selector: "button[type='submit']",
  description: "Nút Đăng nhập"
});

Nhờ vậy, một hàm vẫn có thể dùng lại trong nhiều tình huống mà không mất an toàn kiểu dữ liệu.

 

🔹 4. Function Type - định nghĩa “hình dạng” cho hàm

 

Hàm cũng có cấu trúc riêng: nhận gì vào và trả gì ra. Function Type giúp bạn mô tả điều đó rất rõ ràng.

 

type Operator = (x: number, y: number) => number;

const add: Operator = (a, b) => a + b; // Đúng hợp đồng
const multiply: Operator = (a, b) => a * b; // Đúng hợp đồng

// const invalidOp: Operator = (a: string, b: string) => a + b;
// Lỗi vì tham số không đúng kiểu number

// const anotherInvalidOp: Operator = (a, b) => `Kết quả là ${a + b}`;
// Lỗi vì giá trị trả về là string, không phải number

 

🔹 5. Function Type trong hàm retry

 

Đây là một ví dụ rất hay cho framework automation. Bạn muốn có một hàm retry nhận vào bất kỳ hành động nào, miễn là hành động đó có đúng “hình dạng” mong muốn.

 

type RetryableAction = () => Promise<boolean>;

async function retry(action: RetryableAction, maxAttempts: number): Promise<void> {
  for (let i = 0; i < maxAttempts; i++) {
    console.log(`Đang thử lần ${i + 1}...`);

    const success = await action();

    if (success) {
      console.log("Hành động đã thành công.");
      return; // Dừng ngay nếu thành công
    }
  }

  throw new Error(`Hành động thất bại sau ${maxAttempts} lần thử.`);
}

const clickSubmitButton: RetryableAction = async () => {
  console.log("Đang click nút submit...");
  return Math.random() > 0.5; // Giả lập lúc được lúc không
};

const checkSuccessMessage: RetryableAction = async () => {
  console.log("Đang kiểm tra thông báo thành công...");
  return Math.random() > 0.8; // Giả lập điều kiện khó hơn
};

// await retry(clickSubmitButton, 3);
// await retry(checkSuccessMessage, 5);

Khi tách riêng “hợp đồng của action”, hàm retry của bạn trở nên rất sạch, rất dễ mở rộng và có thể tái sử dụng cho nhiều hành động khác nhau.

 

🏗️ Phần 4: class trong TypeScript - xây “bản thiết kế” an toàn cho framework

 

JavaScript đã có class, nhưng TypeScript nâng nó lên một mức rõ ràng và an toàn hơn bằng typed properties, access modifiers, readonly và cú pháp constructor gọn hơn.

 

🔹 1. Typed Properties - khai báo thuộc tính rõ ràng ngay từ đầu

 

Trong TypeScript, bạn nên khai báo tất cả thuộc tính của class cùng với kiểu dữ liệu của chúng ở đầu class. Cách này giúp người đọc nhìn vào là hiểu object được tạo ra từ class đó sẽ có những gì.

 

class TestUser {
  username: string;
  role: string;
  isLoggedIn: boolean;
  loginAttempts: number;

  constructor(username: string, role: string) {
    this.username = username;
    this.role = role;
    this.isLoggedIn = false; // Gán giá trị mặc định
    this.loginAttempts = 0;
  }

  login(): void {
    console.log(`User ${this.username} đang đăng nhập...`);
    this.isLoggedIn = true;
    this.loginAttempts++;
  }

  logout(): void {
    this.isLoggedIn = false;
  }
}

const myUser = new TestUser("tester01", "QA");
myUser.login();

// myUser.role = 123; // Lỗi: role phải là string
// myUser.email = "test@test.com"; // Lỗi: class này không có thuộc tính email

Đây là kiểu rõ ràng rất có giá trị khi bạn làm việc nhóm hoặc quay lại đọc code sau vài tháng.

 

🔹 2. public, private, protected - kiểm soát quyền truy cập

 

Ba từ khóa này giúp bạn kiểm soát rất chặt việc dữ liệu và phương thức có được truy cập từ bên ngoài hay không.

 

  • public: mặc định, dùng được từ bên ngoài.
  • private: chỉ dùng bên trong chính class đó.
  • protected: dùng được trong class hiện tại và các class con kế thừa từ nó.

 

🔸 private - che giấu logic và trạng thái nội bộ

 

class TestTimer {
  private startTime: number;
  private stopTime: number;

  constructor() {
    this.startTime = 0;
    this.stopTime = 0;
  }

  public start(): void {
    this.startTime = Date.now(); // Chỉ class này được chạm vào startTime
    this.stopTime = 0;
  }

  public stop(): void {
    this.stopTime = Date.now();
  }

  public getDurationInSeconds(): number {
    if (this.startTime === 0 || this.stopTime === 0) {
      return 0;
    }

    return (this.stopTime - this.startTime) / 1000;
  }
}

const timer = new TestTimer();
timer.start();
timer.stop();

// timer.startTime = 123; // Lỗi vì startTime là private

private rất hợp để che giấu các giá trị nhạy cảm, trạng thái trung gian hoặc logic nội bộ mà bạn không muốn code bên ngoài can thiệp trực tiếp.

 

🔸 protected - cực kỳ hữu ích cho base class

 

Đây là công cụ rất hay khi xây framework automation theo hướng có class cha và class con. Class cha giữ logic dùng chung, class con được phép dùng lại, còn bên ngoài thì không.

 

class BasePage {
  protected page: any; // Sau này có thể thay bằng kiểu Page của Playwright

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

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

class HomePage extends BasePage {
  async getTitle(): Promise<string> {
    return await this.page.title(); // Lớp con dùng được thuộc tính protected
  }

  async performSearch(keyword: string): Promise<void> {
    await this.page.fill("#search", keyword);
    await this.takeScreenshot(`search_${keyword}`); // Lớp con gọi được hàm protected
  }
}

// const basePage = new BasePage(page);
// basePage.takeScreenshot("test"); // Lỗi vì takeScreenshot là protected

 

🔹 3. readonly - thuộc tính bất biến

 

Có những giá trị chỉ nên được gán một lần rồi giữ nguyên, ví dụ testId, baseUrl, productId. Với các trường hợp đó, readonly là lựa chọn rất hợp lý.

 

class TestConfig {
  readonly testId: string; // Không cho phép đổi sau khi tạo object
  readonly baseUrl: string = "https://anhtester.com";

  constructor(id: string) {
    this.testId = id;
  }

  changeId(newId: string): void {
    // this.testId = newId; // Lỗi vì testId là readonly
  }
}

 

🔹 4. Parameter Properties - viết constructor gọn hơn rất nhiều

 

TypeScript có một cú pháp rút gọn rất hay: nếu bạn đặt public, private, protected hoặc readonly ngay trước tham số trong constructor, TypeScript sẽ tự tạo thuộc tính tương ứng và tự gán giá trị cho bạn.

 

💻 Cách viết dài:

 

class ProductPageLong {
  private page: any;
  public productId: string;

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

 

💻 Cách viết gọn với parameter properties:

 

class ProductPageShort {
  constructor(
    private page: any, // Tự động tạo thuộc tính private page
    public readonly productId: string // Tự động tạo thuộc tính readonly productId
  ) {}

  async addToCart(): Promise<void> {
    console.log(`Đang thêm sản phẩm ${this.productId} vào giỏ hàng...`);
    await this.page.click("#add-to-cart");
  }
}

// const productPage = new ProductPageShort(page, "SP-001");
// productPage.productId = "SP-002"; // Lỗi vì productId là readonly

Đây là kiểu code bạn sẽ gặp rất thường xuyên trong các dự án TypeScript chuyên nghiệp vì nó ngắn, rõ và giảm lặp rất tốt.

 

🧠 Phần 5: Ghi nhớ nhanh cho Tester

 

  • type phù hợp với union, function type, tuple, alias cho primitive và dữ liệu test.
  • interface phù hợp với object, class và các “hợp đồng” trong OOP.
  • Nếu bài toán là Page Object Model hoặc cấu trúc class, hãy nghiêng về interface.
  • Nếu bài toán là kiểu trạng thái, callback, union hoặc dữ liệu linh hoạt, hãy nghiêng về type.
  • Union Type giúp utility function linh hoạt hơn, nhưng thường phải đi kèm type narrowing.
  • Function Type giúp chuẩn hóa callback, action và các helper như retry.
  • private che giấu logic nội bộ, protected phục vụ kế thừa, còn public là mức mặc định.
  • readonly rất hợp cho ID, config và các giá trị không nên bị thay đổi trong vòng đời object.
  • Parameter properties là cú pháp ngắn gọn nhưng cực kỳ thực dụng khi viết class trong TypeScript.

 

Kết luận: Bài 10 là bước nâng cấp rất mạnh về tư duy TypeScript. Khi hiểu rõ type, interface, union, function typeclass với private, protected, readonly, bạn sẽ viết framework automation chắc tay hơn, an toàn hơn và dễ bảo trì hơn rất nhiều.

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