NỘI DUNG BÀI HỌC
-
🚀 Linh hoạt & Vững chắc: Nắm vững cách dùng
Type Alias
vàInterface
để đị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ớiFunction 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: TYPE ALIASES VÀ INTERFACES - ĐẶT TÊN CHO KIỂU DỮ LIỆU
Trong TypeScript, việc định nghĩa rõ ràng "hình dạng" của dữ liệu là một trong những tính năng mạnh mẽ nhất. Nó giúp chúng ta viết code an toàn hơn, dễ đọc hơn và giảm thiểu lỗi ngay từ khi gõ phím. Hai công cụ chính để đạt được điều này là Type Aliases (type) và Interfaces. Hãy cùng tìm hiểu sâu hơn về chúng.
Type Alias - Bậc Thầy của sự Linh hoạt
Type Alias, đúng như tên gọi, cho phép bạn tạo một "biệt danh" hay một tên mới cho bất kỳ kiểu dữ liệu nào. Sự linh hoạt của nó là điểm mạnh lớn nhất: bạn có thể dùng nó cho các kiểu đơn giản như string cho đến các cấu trúc phức tạp như object, union, hay tuple.
Định nghĩa và Cú pháp
Sử dụng từ khóa type để tạo một biệt danh.
// Thay vì lặp đi lặp lại một cấu trúc phức tạp:
let user1: { name: string; id: number; isActive: boolean; };
let user2: { name: string; id: number; isActive: boolean; };
// Bạn tạo một "biệt danh" User có thể tái sử dụng ở bất kỳ đâu:
type User = {
readonly id: number; // Chỉ đọc, không thể thay đổi sau khi khởi tạo
name: string;
role?: string; // Thuộc tính tùy chọn, có thể có hoặc không
isActive: boolean;
};
// Và bây giờ, việc sử dụng trở nên cực kỳ gọn gàng và rõ ràng:
let user1: User = { id: 1, name: "Admin", isActive: true };
let user2: User = { id: 2, name: "Guest", role: "Viewer", isActive: false };
🧠 Ví von: Hãy nghĩ type giống như việc bạn tạo một "phím tắt" trên máy tính. Thay vì mỗi lần phải truy cập một đường dẫn dài như C:\Users\YourName\Documents\Projects\Automation, bạn chỉ cần nhấn vào phím tắt "My Project" trên desktop. type cũng vậy, nó là phím tắt cho những kiểu dữ liệu phức tạp.
Các Ứng dụng Vượt trội của Type Alias
type không chỉ dùng cho object. Đây là nơi sức mạnh thực sự của nó tỏa sáng:
Union Types (Kiểu kết hợp): Định nghĩa một kiểu có thể là một trong nhiều khả năng.
type Status = "Passed" | "Failed" | "Skipped" | "Pending";
type TestResult = {
testCaseId: string;
status: Status; // Chỉ có thể là 1 trong 4 giá trị trên
};
const result1: TestResult = { testCaseId: "TC-01", status: "Passed" };
// const result2: TestResult = { testCaseId: "TC-02", status: "Error" }; // Lập tức báo lỗi!
Kiểu Nguyên thủy (Primitives): Tạo ra các kiểu rõ ràng hơn về mặt ngữ nghĩa.
type Email = string;
type UserId = number;
function sendEmailTo(email: Email, userId: UserId) {
// Code rõ ràng hơn nhiều so với (email: string, userId: number)
}
Ứng dụng trong Test Automation
Type Alias là lựa chọn hoàn hảo để định nghĩa cấu trúc dữ liệu test. Nó giúp đảm bảo mọi bộ dữ liệu đều tuân thủ một định dạng nhất quán.
// Định nghĩa cấu trúc cho dữ liệu test của một test case đăng nhập
type LoginTestData = {
readonly testId: string; // Dữ liệu test không nên bị thay đổi giữa chừng
description: string;
username: string;
password: string;
expectedMessage: string;
};
// Tạo một mảng các bộ dữ liệu test với kiểu đã định nghĩa
const loginScenarios: LoginTestData[] = [
{
testId: "TC-01",
description: "Verify login with valid credentials",
username: "tomsmith",
password: "SuperSecretPassword!",
expectedMessage: "You logged into a secure area!"
},
{
testId: "TC-02",
description: "Verify login with invalid username",
username: "wronguser",
password: "SuperSecretPassword!",
expectedMessage: "Your username is invalid!"
}
];
Interface - "Hợp đồng" Vững chắc cho Lập trình Hướng đối tượng
Interface cũng là một cách để đặt tên cho một cấu trúc object. Tuy nhiên, nó được thiết kế với tư tưởng hướng đối tượng mạnh mẽ hơn, hoạt động như một "hợp đồng" (contract) mà các object hoặc class phải tuân thủ.
Định nghĩa và Cú pháp
Sử dụng từ khóa interface.
interface Person {
name: string;
age: number;
greet(): void; // Một phương thức phải có
}
🧠 Ví von: interface giống như một "bản mô tả công việc" cho vị trí Lập trình viên. Nó yêu cầu ứng viên (class/object) phải có các kỹ năng (thuộc tính) như "biết code" (name: string), có "kinh nghiệm" (age: number), và phải "biết giao tiếp" (greet(): void). Bất kỳ ai muốn ứng tuyển vào vị trí này đều phải đáp ứng đủ các yêu cầu đó.
Các Tính năng Đặc trưng
Kế thừa (Extends): Interface có thể kế thừa từ một hoặc nhiều interface khác, tạo ra các hợp đồng phức tạp và chuyên biệt hơn.
interface Employee extends Person { // Employee kế thừa mọi thứ từ Person
employeeId: string;
department: string;
}
let emp: Employee = {
name: "John",
age: 30,
employeeId: "E123",
department: "Engineering",
greet: () => console.log("Hello!")
};
Hợp nhất Khai báo (Declaration Merging): Đây là điểm khác biệt độc đáo nhất. Nếu bạn định nghĩa một interface có cùng tên ở nhiều nơi, TypeScript sẽ tự động gộp tất cả các thuộc tính lại thành một.
interface Box {
height: number;
width: number;
}
interface Box {
length: number;
// Có thể thêm thuộc tính mới, không được định nghĩa lại thuộc tính cũ
}
// TypeScript sẽ hiểu Box là: { height: number; width: number; length: number; }
let myBox: Box = { height: 10, width: 20, length: 30 };
Tính năng này rất hữu ích khi làm việc với các thư viện bên ngoài mà bạn muốn bổ sung thêm thuộc tính cho các interface có sẵn của họ.
Ứng dụng trong Test Automation
Interface là lựa chọn hàng đầu khi định nghĩa cấu trúc cho các class trong mô hình Page Object Model (POM).
// Định nghĩa một "hợp đồng" chung cho tất cả các Page Object
// Quy ước chung thường thêm chữ "I" ở đầu tên interface
interface IPageObject {
page: any; // Tạm thời dùng any, sau này sẽ là kiểu Page của Playwright
// Mọi trang đều phải có phương thức điều hướng tới nó
goTo(): Promise<void>;
// Mọi trang đều phải có phương thức kiểm tra xem đã ở đúng trang chưa
assertIsOnPage(): Promise<void>;
}
// Class LoginPage "ký hợp đồng" (implements) và BẮT BUỘC phải thực thi các yêu cầu của IPageObject
class LoginPage implements IPageObject {
page: any;
constructor(page: any) {
this.page = page;
}
async goTo(): Promise<void> {
await this.page.goto("/login");
}
async assertIsOnPage(): Promise<void> {
// Code kiểm tra URL hoặc một element đặc trưng của trang login
}
// ... các phương thức và locator khác của trang LoginPage
}
## Type Alias vs. Interface: Cuộc đối đầu Thân thiện
Vậy cuối cùng, khi nào nên dùng cái nào?
Tiêu chí |
Type Alias (type) |
Interface |
Nên dùng cho |
Mọi loại kiểu dữ liệu, đặc biệt mạnh với Union (` |
), **Intersection** (&`), Tuple. |
Khả năng mở rộng |
Dùng & (Intersection) để kết hợp các type. |
Dùng extends (Kế thừa), tự nhiên hơn trong OOP. |
Hợp nhất Khai báo |
Không thể. Một type chỉ được định nghĩa một lần. |
Có thể. TypeScript tự động gộp các interface cùng tên. |
Quy tắc Vàng để Lựa chọn 💡
Bạn đang định nghĩa cấu trúc cho một Object hay một Class?
Hãy bắt đầu với interface. Cú pháp extends và implements rất tự nhiên trong lập trình hướng đối tượng (ví dụ: POM).
Bạn có cần dùng Union (|), Intersection (&), hay Tuple [string, number] không?
Bạn bắt buộc phải dùng type.
Bạn có đang viết một thư viện và muốn người dùng có thể mở rộng định nghĩa của bạn không?
Hãy dùng interface để tận dụng tính năng "Hợp nhất Khai báo".
Lời khuyên cuối cùng cho người mới bắt đầu:
Hãy ưu tiên interface khi định nghĩa cấu trúc cho các object và class. Dùng type cho tất cả các trường hợp còn lại.
Việc tuân thủ quy ước này sẽ giúp code của bạn nhất quán và dễ đọc hơn cho cả bạn và đồng nghiệp trong tương lai. Cả hai đều là những công cụ tuyệt vời, và việc hiểu rõ khi nào nên dùng công cụ nào sẽ giúp bạn trở thành một lập trình viên TypeScript hiệu quả hơn.
PHẦN 2: UNION TYPES VÀ FUNCTION TYPES - TẠO KIỂU DỮ LIỆU ĐA NĂNG
Union Types (|) - Kiểu "hoặc là... hoặc là"
Union Type là một tính năng cho phép một biến, tham số hoặc thuộc tính có thể chứa giá trị thuộc một trong nhiều kiểu dữ liệu khác nhau. Đây là cách để bạn nói với TypeScript rằng: "Giá trị ở đây có thể là string, hoặc có thể là number, hoặc thậm chí là một object cụ thể".
Định nghĩa chung
Cú pháp: Sử dụng dấu gạch dọc (|) để ngăn cách giữa các kiểu.
Ví von: 🧠 Union Type giống như một cổng sạc đa năng. Nó có thể chấp nhận đầu cắm USB-A (string), USB-C (number), hay thậm chí là Lightning (boolean). Bất kỳ loại cáp nào trong số đó đều hợp lệ.
Ví dụ cơ bản:
let userIdentifier: string | number;
userIdentifier = 101; // Hợp lệ
userIdentifier = "auth-xyz-789"; // Hợp lệ
// userIdentifier = true; // Lỗi! Kiểu boolean không nằm trong danh sách cho phép (string | number).
Sức mạnh của Type Narrowing (Thu hẹp kiểu)
Điều tuyệt vời nhất khi làm việc với Union Type là TypeScript sẽ bảo vệ bạn. Nó sẽ không cho phép bạn sử dụng các phương thức chỉ thuộc về một kiểu cụ thể cho đến khi bạn kiểm tra và xác nhận kiểu dữ liệu hiện tại của biến. Quá trình này được gọi là Type Narrowing.
function processIdentifier(id: string | number) {
// console.log(id.toUpperCase()); // Lỗi! TypeScript cảnh báo: 'toUpperCase' có thể không tồn tại trên kiểu 'number'.
// Bắt đầu "thu hẹp kiểu"
if (typeof id === "string") {
// Bên trong khối if này, TypeScript thông minh và biết chắc chắn id là một string.
// Vì vậy, mọi phương thức của string đều hợp lệ.
console.log("ID dạng chuỗi:", id.toUpperCase());
} else {
// Nếu không phải string, TypeScript suy luận id phải là number.
console.log("ID dạng số:", id.toFixed(2)); // toFixed() là phương thức của number
}
}
Ứng dụng trong Automation
Union Types cực kỳ hữu ích khi bạn muốn tạo ra các hàm tiện ích (utility functions) linh hoạt, có thể chấp nhận nhiều loại đầu vào khác nhau, giúp giảm thiểu việc viết các hàm trùng lặp.
Kịch bản: Bạn muốn viết một hàm findElement có thể tìm kiếm một phần tử trên trang web bằng nhiều cách:
Dùng một CSS selector (kiểu string).
Dùng một đối tượng Locator đã được định nghĩa sẵn (kiểu object).
Ví dụ Automation:
// Giả sử ta có một kiểu Locator từ Playwright
type Locator = { selector: string; description: string; };
function findElement(locator: string | Locator) {
if (typeof locator === "string") {
console.log(`Đang tìm phần tử bằng CSS selector: "${locator}"`);
// Code Playwright tương ứng: return page.locator(locator);
} else {
console.log(`Đang tìm phần tử được định nghĩa sẵn: "${locator.description}"`);
// Code Playwright tương ứng: return page.locator(locator.selector);
}
}
// Sử dụng hàm theo cả hai cách
findElement("#username"); // Tìm trực tiếp bằng selector
const loginButton: Locator = {
selector: "button[type='submit']",
description: "Nút Đăng nhập"
};
findElement(loginButton); // Tìm bằng đối tượng đã định nghĩa
<hr size=2 width="100%" noshade style='color:gray' align=center>
Function Types - Định nghĩa "hình dạng" cho Hàm
Function Types cho phép bạn định nghĩa một "mẫu" hay một "hợp đồng" cho một hàm. Nó không quan tâm hàm đó làm gì bên trong, nó chỉ quan tâm đến việc hàm đó nhận vào những tham số (với kiểu dữ liệu) nào và trả về kết quả có kiểu dữ liệu là gì.
Định nghĩa chung
Cú pháp: Thường được định nghĩa bằng type alias.
(param1: type1, param2: type2) => returnType
Ví von: 🧠 Nó giống như việc bạn thiết kế một khuôn làm bánh. Khuôn quy định rằng bạn phải đổ vào bột (tham số 1) và trứng (tham số 2), và sản phẩm cuối cùng phải là một chiếc bánh bông lan (kiểu trả về). Bất kỳ ai sử dụng khuôn này đều phải tuân thủ công thức đó.
Ví dụ cơ bản:
// Định nghĩa một kiểu hàm 'Operator'
// Nó mô tả một hàm nhận vào 2 tham số number và phải trả về một number
type Operator = (x: number, y: number) => number;
// Khai báo các hàm tuân thủ đúng "hợp đồng" của Operator
const add: Operator = (a, b) => a + b;
const multiply: Operator = (a, b) => a * b;
// Hàm này sẽ BÁO LỖI vì nó không tuân thủ hợp đồng:
// Nó nhận vào string, không phải number
// const invalidOp: Operator = (s1: string, s2: string) => s1 + s2;
// Hàm này cũng BÁO LỖI vì nó trả về string, không phải number
// const anotherInvalidOp: Operator = (a, b) => `Kết quả là ${a + b}`;
Ứng dụng trong Automation
Function Types rất mạnh mẽ để tạo ra các kiến trúc code có tính module hóa và dễ mở rộng, đặc biệt là trong các kịch bản cần sự linh hoạt trong việc thực thi hành động.
Kịch bản: Trong một framework kiểm thử, bạn muốn tạo một hàm retry có khả năng thử lại bất kỳ hành động (action) nào cho đến khi nó thành công hoặc hết số lần thử. "Hành động" ở đây có thể là bất cứ thứ gì: click vào một nút, điền vào một form, kiểm tra một văn bản...
Ví dụ Automation:
// 1. Định nghĩa "hình dạng" của một hành động có thể được thử lại.
// Nó là một hàm không nhận tham số và trả về một Promise<boolean> (true nếu thành công, false nếu thất bại).
type RetryableAction = () => Promise<boolean>;
// 2. Tạo hàm `retry` chấp nhận một hành động có kiểu RetryableAction
async function retry(action: RetryableAction, maxAttempts: number): Promise<void> {
for (let i = 0; i < maxAttempts; i++) {
console.log(`Đang thử lại lần thứ ${i + 1}...`);
const success = await action();
if (success) {
console.log("Hành động thành công!");
return; // Thoát khỏi hàm
}
// Có thể thêm đoạn chờ ngắn ở đây trước khi thử lại
}
throw new Error(`Hành động thất bại sau ${maxAttempts} lần thử.`);
}
// 3. Tạo các hành động cụ thể tuân thủ hợp đồng RetryableAction
const clickSubmitButton: RetryableAction = async () => {
// Giả sử page.click() trả về true nếu thành công
console.log("...Đang click nút submit");
// return await page.locator("#submit").click();
return Math.random() > 0.5; // Giả lập thành công/thất bại ngẫu nhiên
};
const checkSuccessMessage: RetryableAction = async () => {
console.log("...Đang kiểm tra thông báo thành công");
// return await page.locator(".success-message").isVisible();
return Math.random() > 0.8; // Giả lập hành động này khó thành công hơn
};
// 4. Sử dụng hàm retry với các hành động khác nhau
// await retry(clickSubmitButton, 3);
// await retry(checkSuccessMessage, 5);
Bằng cách này, hàm retry của bạn trở nên cực kỳ linh hoạt. Bạn có thể truyền bất kỳ hành động nào vào nó, miễn là hành động đó tuân thủ đúng "hình dạng" mà RetryableAction đã định nghĩa.
PHẦN 3: CLASS TRONG TYPESCRIPT - XÂY DỰNG CÁC "BẢN THIẾT KẾ" AN TOÀN
Lập trình Hướng đối tượng (OOP) là một phương pháp mạnh mẽ để tổ chức code, và class chính là công cụ trung tâm của phương pháp này. Nếu JavaScript cung cấp cho chúng ta "bản thiết kế" cơ bản, thì TypeScript nâng cấp nó thành một "bản thiết kế kiến trúc" chi tiết, an toàn và cực kỳ rõ ràng.
Khai báo Thuộc tính với Kiểu dữ liệu (Typed Properties)
Đây là bước cải tiến nền tảng và quan trọng nhất. Trong TypeScript, bạn phải khai báo tất cả các thuộc tính của class cùng với kiểu dữ liệu của chúng ở cấp cao nhất, trước cả constructor. Điều này tạo ra một "hợp đồng" rõ ràng về hình dạng của mọi đối tượng được tạo ra từ class đó.
class TestUser {
// 1. Khai báo thuộc tính và kiểu dữ liệu tường minh
username: string;
role: string;
isLoggedIn: boolean;
loginAttempts: number;
// 2. constructor có nhiệm vụ khởi tạo giá trị cho các thuộc tính đã khai báo
constructor(username: string, role: string) {
this.username = username;
this.role = role;
this.isLoggedIn = false; // Gán giá trị mặc định cho các thuộc tính còn lại
this.loginAttempts = 0;
}
// 3. Các phương thức làm việc với các thuộc tính đã định nghĩa
login(): void {
console.log(`User ${this.username} is logging in...`);
this.isLoggedIn = true;
this.loginAttempts++;
}
logout(): void {
this.isLoggedIn = false;
}
}
const myUser = new TestUser("tester01", "QA");
myUser.login();
// TypeScript sẽ bảo vệ bạn khỏi các lỗi ngớ ngẩn:
// myUser.role = 123; // LỖI NGAY LẬP TỨC! Type 'number' is not assignable to type 'string'.
// myUser.email = "test@test.com"; // LỖI! Property 'email' does not exist on type 'TestUser'.
Lợi ích: Bất cứ ai đọc code cũng biết ngay lập tức một TestUser có những dữ liệu gì và phải tuân thủ kiểu nào, giúp việc bảo trì và làm việc nhóm trở nên dễ dàng hơn rất nhiều.
Các từ khóa Truy cập (Access Modifiers)
Đây là nơi TypeScript thực sự tỏa sáng trong OOP, giúp bạn triển khai Tính Đóng gói (Encapsulation) một cách hoàn hảo. Các từ khóa này kiểm soát mức độ truy cập vào các thuộc tính và phương thức của một class.
🧠 Ví von: Hãy coi class như một tòa nhà văn phòng và các từ khóa này là các cấp độ thẻ an ninh:
public: Thẻ khách 🧍♂️ - Có thể đi vào sảnh chính và các khu vực công cộng.
protected: Thẻ nhân viên 🔑 - Có thể vào khu vực làm việc của công ty và các phòng ban liên quan.
private: Thẻ an ninh cấp cao 🔒 - Chỉ có thể vào phòng server hoặc phòng thí nghiệm nội bộ.
public (Mặc định)
Nếu không khai báo gì, mọi thứ đều là public. Bạn có thể truy cập và thay đổi chúng từ bất cứ đâu.
private - "Nội bất xuất, ngoại bất nhập"
Một thuộc tính hay phương thức private chỉ có thể được truy cập từ bên trong chính class đó. Đây là cách tốt nhất để che giấu logic nội bộ, các trạng thái nhạy cảm hoặc các hàm tiện ích mà bạn không muốn bên ngoài can thiệp vào.
class TestTimer {
private startTime: number; // Chỉ class này mới biết về sự tồn tại của startTime
private stopTime: number;
constructor() {
this.startTime = 0;
this.stopTime = 0;
}
public start(): void {
this.startTime = Date.now();
this.stopTime = 0; // Reset stopTime khi bắt đầu lại
}
public stop(): void {
this.stopTime = Date.now();
}
public getDurationInSeconds(): number {
if (this.startTime === 0 || this.stopTime === 0) {
return 0;
}
// Logic tính toán được che giấu bên trong
return (this.stopTime - this.startTime) / 1000;
}
}
const timer = new TestTimer();
timer.start();
// timer.startTime = 123; // LỖI! Property 'startTime' is private.
// ... chờ một lúc ...
timer.stop();
console.log(`Thời gian đã trôi qua: ${timer.getDurationInSeconds()} giây.`);
protected - Cho phép Kế thừa
Một thành viên protected có thể được truy cập từ class chứa nó và các class con kế thừa từ nó, nhưng không thể truy cập từ bên ngoài. Đây là công cụ hoàn hảo để tạo ra các class cơ sở (base class) trong automation framework.
// Base class cho tất cả các Page Object
class BasePage {
// Thuộc tính 'page' được bảo vệ, chỉ các lớp con mới được dùng
protected page: any; // Sau này sẽ là kiểu 'Page' của Playwright
constructor(page: any) {
this.page = page;
}
protected async takeScreenshot(fileName: string): Promise<void> {
// Một hàm tiện ích nội bộ mà các trang con có thể dùng
await this.page.screenshot({ path: `screenshots/${fileName}.png` });
}
}
class HomePage extends BasePage {
async getTitle(): Promise<string> {
// OK! Lớp con có thể truy cập thuộc tính 'protected' của lớp cha.
return await this.page.title();
}
async performSearch(keyword: string): Promise<void> {
await this.page.fill("#search", keyword);
// OK! Lớp con có thể gọi phương thức 'protected' của lớp cha.
await this.takeScreenshot(`search_${keyword}`);
}
}
// const basePage = new BasePage(page);
// basePage.takeScreenshot("test"); // LỖI! Property 'takeScreenshot' is protected.
Các Tính năng Nâng cao Hữu ích khác
readonly - Thuộc tính Bất biến
Từ khóa này tạo ra một thuộc tính chỉ có thể được gán giá trị một lần duy nhất, thường là ngay tại lúc khai báo hoặc bên trong constructor. Nó rất hữu ích cho các giá trị không bao giờ nên thay đổi trong suốt vòng đời của đối tượng.
class TestConfig {
readonly testId: string; // ID của test case, không bao giờ thay đổi
readonly baseUrl: string = "https://ecommerce-playground.lambdatest.io/";
constructor(id: string) {
this.testId = id;
}
changeId(newId: string) {
// this.testId = newId; // LỖI! Cannot assign to 'testId' because it is a read-only property.
}
}
Parameter Properties - Phím tắt cho constructor
TypeScript cung cấp một cú pháp cực kỳ gọn gàng để vừa khai báo thuộc tính, vừa khởi tạo nó trong constructor chỉ bằng một bước. Bằng cách thêm một từ khóa truy cập (public, private, protected) hoặc readonly trước một tham số trong constructor, TypeScript sẽ tự động:
Tạo ra một thuộc tính có tên tương ứng trong class.
Gán giá trị của tham số đó cho thuộc tính vừa tạo.
// === Cách dài dòng (như ở trên) ===
class ProductPage_Long {
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 ProductPage_Short {
// Tự động tạo ra thuộc tính 'page' (private) và 'productId' (public, readonly)
constructor(
private page: any,
public readonly productId: string
) {}
async addToCart() {
// this.productId = "new-id"; // LỖI! vì là readonly
console.log(`Adding product ${this.productId} to cart.`);
await this.page.click("#add-to-cart");
}
}
Cả hai class trên đều hoạt động y hệt nhau, nhưng phiên bản _Short gọn hơn rất nhiều. Đây là cách viết rất phổ biến trong các dự án TypeScript chuyên nghiệp.