NỘI DUNG BÀI HỌC
-
🚀 Linh hoạt & Vững chắc: Nắm vững cách dùng
Type AliasvàInterfaceđể định hình dữ liệu. -
🧩 Kiểu Đa năng: Khám phá
Union Typesvà định nghĩa "hợp đồng" cho hàm vớiFunction Types. -
🛡️ OOP An toàn: Xây dựng Class chuyên nghiệp với
private,protectedvà các cú pháp nâng cao.
🧩 Phần 1: type và interface - đặ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à type và interface. 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à string và UserId 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: vì
extendsvàimplementsđọ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ỉ
interfacelà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:
typethườ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ớitype.
🔀 Phần 3: Union Types và Function 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 Type và Function 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
typephù hợp với union, function type, tuple, alias cho primitive và dữ liệu test.interfacephù 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 Typegiúp utility function linh hoạt hơn, nhưng thường phải đi kèm type narrowing.Function Typegiúp chuẩn hóa callback, action và các helper nhưretry.privateche giấu logic nội bộ,protectedphục vụ kế thừa, cònpubliclà mức mặc định.readonlyrấ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 type và class 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.
