[TypeScript] Tìm hiểu về Classes
Bài đăng này đã không được cập nhật trong 4 năm

TypeScript là một ngôn ngữ mã nguồn mở miễn phí hiện đang được phát triển và bảo trì bởi Microsoft. Nó là superset của JavaScript, với các bổ sung các tuỳ chọn kiểu tĩnh và lớp trên cơ sở lập trình hướng đối tượng cho ngôn ngữ này. Vì TypeScript là tập cha của JavaScript nên bất kì chương trình JavaScript nào đã có cũng đều là chương trình TypeScript hợp lệ. Trong bài viết này, ta hãy cùng đi tìm hiểu về Classes trong TypeScript để thấy được tính super của nó so với JavaScript nhé.
1. Giới thiệu
TypeScript , giống như ES6, hỗ trợ lập trình hướng đối tượng bằng cách sử dụng các lớp. Điều này trái ngược với các phiên bản JavaScript cũ hơn, vốn chỉ hỗ trợ chuỗi kế thừa dựa trên nguyên mẫu (prototype). Hỗ trợ về class trong TypeScript tương tự như trong các ngôn ngữ Java hay C#, mà trong đó, các lớp có thể kế thừa từ các lớp khác, trong khi các đối tượng được khởi tạo dưới dạng thể hiện của lớp.
Lớp trong TypeScript được khai báo thêm các chú thích kiểu dữ liệu vào các thuộc tính và phương thức của lớp. Ví dụ sau đây là khai báo cho class Car trong TypeScript
class Car {
name: string;
brand: string;
price: number;
constructor(name: string, brand: string, price: number) {
this.name = name;
this.brand = brand;
this.price = price;
}
getCarInfo(): string {
return `${this.name} ${this.brand} cost ${this.price}`;
}
}
Khi bạn chú thích kiểu dữ liệu cho thuộc tính, hàm khởi tạo (constructor) và phương thức, trình biên dịch TypeScript sẽ thực hiện các kiểm tra kiểu dữ liệu tương ứng.
2. Access Modifier
Chỉ thị truy cập (access modifier) thay đổi khả năng truy cập của các thuộc tính và phương thức của một lớp (class). TypeScript cung cấp cho chúng ta 4 từ khóa access modifier
publicprivateprotectedreadonly
Để hiểu rõ hơn, cùng xét một ví dụ sau đây:
class Car {
private name: string;
protected brand: string;
readonly price: number;
constructor(name: string, brand: string, price: number) {
this.name = name;
this.brand = brand;
this.price = price;
}
public getCarInfo(): string {
return `${this.name} ${this.brand} cost ${this.price}`;
}
}
Public
Với public , bạn có thể thoải mái gọi thuộc tính (property) hay phương thức (method) từ bất cứ đâu, trong hay ngoài class đều được.
Nếu một thuộc tính hoặc phương thức
không được modifierthì mặc định sẽ làpublic
Ở ví dụ trên, phương thức getInfo() là public nên bạn hoàn toàn có thể truy cập được vào nó từ bên ngoài class
let car = new Car("E450", "Mercedes", 50000);
console.log(car.getInfo());
// E450 Mercedes cost 50000
Private
Ngược lại với public, với private thì bạn chỉ có thể truy cập từ bên trong class đó
Thuộc tính name của class Car được modifier là private, nên khi ta truy cập từ bên ngoài sẽ báo lỗi như hình dưới đây:

Protected
Với protected, nó chỉ cho phép truy cập từ bên trong cùng một lớp (giống private) và trong các lớp con kế thừa của nó
Thuộc tính brand là protected nên chỉ có thể truy cập từ lớp kế thừa của nó. Ta có thể làm như sau:
class MyCar extends Car {
getMyCar(): string {
return this.brand;
}
}
let myCar = new MyCar("I8", "BMW", 45000);
console.log(myCar.getMyCar());
// BMW
Readonly
Ngoài ra, trong TypeScript, bạn còn có thêm một modifier mới là readonly. Khi một thuộc tính có thêm readonly, có nghĩa là thuộc tính này chỉ có thể lấy được giá trị của chúng mà không thể thay đổi giá trị.
Lưu ý là ta cần phải gán giá trị cho thuộc tính có modifier là readonly thì mới khởi tạo nó hoặc gán nó cho hàm constructor, hoặc không nó sẽ báo lỗi
Trong ví dụ trên, thuộc tính price được modifier là readonly. Chính vì vậy, ta chỉ có thể lấy giá trị ra mà không thể thay đổi giá trị của nó

3. Getter & Setter
Đối với mỗi thuộc tính:
Phương thức gettertrả về giá trị của thuộc tính. Một getter còn được gọi là một accessorPhương thức settercập nhật giá trị của thuộc tính. Một setter còn được gọi là một mutator
Phương thức getter bắt đầu với từ khóa get và phương thức setter bắt đầu với từ khóa set
Cách triển khai getter và setter:
- Trước hết, cập nhật chỉ thị truy cập (access modifier) cho các thuộc tính (properties) từ
publicthànhprivate - Sau đó, thay đổi tên của thuộc tính từ dạng
namePropertysang thành dạng_nameProperty - Cuối cùng, tạo phương thức
gettervàsettercho thuộc tính
Để hiểu rõ hơn, hãy cùng mình làm thử một ví dụ dưới đây nhé:
Ta tạo một lớp Person đơn giản với 3 thuộc tính: age, firstName và lastName
class Person {
public age: number;
public firstName: string;
public lastName: string;
Ở đây, thuộc tính age hoàn toàn có thể được truy cập từ bên ngoài. Giả sử ta muốn kiểm soát tính hợp lệ đầu vào do người dùng nhập để gán cho thuộc tính age. Ta có thể kiểm tra trước khi gán như sau:
if(inputAge > 0 && inputAge <= 200) {
// Thực hiện gán giá trị cho thuộc tính age
}
Nhưng, nếu sử dụng đoạn mã này để kiểm tra ở khắp mọi nơi thì rất dư thừa và sẽ rất khó đọc, khó quản lý code. Đây chính là lúc sử dụng sự tối ưu của setter và getter. Ta sẽ có đoạn mã sử dụng getter và setter cho cả 3 thuộc tính age, firstName và lastName:
class Person {
private _age: number;
private _firstName: string;
private _lastName: string;
public get age() {
return this._age;
}
public set age(theAge: number) {
if (theAge <= 0 || theAge >= 200) {
throw new Error("The age is invalid");
}
this._age = theAge;
}
public get firstName() {
return this._firstName;
}
public set firstName(theFirstName: string) {
if (!theFirstName) {
throw new Error("Invalid first name.");
}
this._firstName = theFirstName;
}
public get lastName() {
return this._lastName;
}
public set lastName(theLastName: string) {
if (!theLastName) {
throw new Error("Invalid last name.");
}
this._lastName = theLastName;
}
public getFullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
Lúc này, nếu ta muốn gán một giá trị không hợp lệ cho thuộc tính age thì sẽ lỗi ngay
let person = new Person();
// person.age = 10;
person.age = 0; // Error: The age is invalid
4. Inheritance (Kế thừa) trong TypeScript
Một lớp có thể sử dụng lại các thuộc tính và phương thức của một lớp khác. Đây gọi là kế thừa (Inheritance)
Lớp kế thừa các thuộc tính và phương thức được gọi là lớp con. Và lớp có các thuộc tính và phương thức được kế thừa gọi là lớp cha.
Kế thừa cho phép ta sử dụng lại các thuộc tính và phương thức của một lớp hiện có mà không cần viết lại cho nó.
JavaScript hỗ trợ prototype inheritance, không phải dạng kế thừa cổ điển như Java hay C#. ES6 cung cấp cú pháp class đơn giản là cú pháp kế thừa của prototype. TypeScript hỗ trợ kế thừa giống như ES6.
Để kể thừa một lớp, ta cũng sử dụng từ khóa quen thuộc là extends
class Person {
constructor(private firstName: string, private lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
}
getFullName(): string {
return `${this.firstName} ${this.lastName}`;
}
describe(): string {
return `This is ${this.firstName} ${this.lastName}.`;
}
}
Lớp Employee kế thừa từ lớp Person
class Employee extends Person {
//..
}
let newEmployee = new Employee("Quynh", "Nhu");
console.log(newEmployee.getFullName()); // Quynh Nhu
Trong ví dụ trên, Employee là lớp con và Person là lớp cha
5. Constructor
Vì lớp Person có một phương thức khởi tạo (constructor) có các thuộc tính firstName và lastName, ta có thể khởi tạo các thuộc tính này trong phương thức khởi tạo của lớp Employee bằng cách gọi phương thức khởi tạo lớp cha của nó
Để gọi hàm khởi tạo của lớp cha trong hàm khởi tạo của lớp con, ta sử dụng cú pháp super()
class Employee extends Person {
constructor(firstName: string, lastName: string, private jobTitle: string) {
// call the constructor of the Person class:
super(firstName, lastName);
}
}
Đây là một thể hiện của lớp Employee
let employee = new Employee("Tung", "Hoang", "NodeJSDev");
console.log(employee);
/*
Employee {
firstName: 'Tung',
lastName: 'Hoang',
jobTitle: 'NodeJSDev'
}
*/
console.log(employee.getFullName()); // Tung Hoang
console.log(employee.describe()); // This is Tung Hoang.
6. Method Overriding
Khi ta gọi phương thức employee.describe() trên đối tượng Employee, phương thức describe() của lớp Person sẽ được thực thi và hiển thị ra thông điệp theo cách mà ta đã định nghĩa trong lớp Person
Nhưng nếu ta muốn lớp Employee có một phiên bản riêng của nó cho phương thức describe(), ta có thể ghi đè phương thức (method overriding) trong lớp Employee như sau:
class Employee extends Person {
constructor(firstName: string, lastName: string, private jobTitle: string) {
super(firstName, lastName);
}
describe(): string {
return super.describe() + `I'm a ${this.jobTitle}.`;
}
}
Trong phương thức describe(), ta sử dụng cú pháp super.methodInParentClass()
Lúc này, khi ta gọi phương thức describe() trên đối tượng employee thì phương thức describe() của riêng lớp Employee được gọi đến:
let employee = new Employee("Tung", "Hoang", "NodeJSDev");
console.log(employee.describe()); // This is Tung Hoang. I'm a NodeJSDev.
7. Phương thức và thuộc tính static trong TypeScript
Thuộc tính static
Không giống như thuộc tính thông thường, thuộc tính static được chia sẻ giữa tất cả các thể hiện của một lớp
Để khai báo thuộc tính tĩnh, ta sử dụng từ khóa static. Để truy cập thuộc tính tĩnh, ta sử dụng cú pháp className.propertyName
class Employee {
static headcount: number = 0;
constructor(
private firstName: string,
private lastName: string,
private jobTitle: string
) {
Employee.headcount++;
}
}
let tung = new Employee("Tung", "Hoang", "NodeJSDev");
let minh = new Employee("Quynh", "Nhu", "QA");
console.log(Employee.headcount); // 2
}
Trong ví dụ trên, thuộc tính tĩnh headcount được khởi tạo bằng 0, giá trị của nó sẽ tăng lên 1 bất cứ khi nào một đối tượng mới được tạo
Phương thức static
Tương tự như thuộc tính static
class Employee {
private static headcount: number = 0;
constructor(
private firstName: string,
private lastName: string,
private jobTitle: string
) {
Employee.headcount++;
}
public static getHeadCount() {
return Employee.headcount;
}
}
let tung = new Employee("Tung", "Hoang", "NodeJSDev");
let minh = new Employee("Quynh", "Nhu", "QA");
console.log(Employee.getHeadCount); // 2
Trong ví dụ này, ta đã thay đổi chỉ thị truy cập của thuộc tính tĩnh headcount từ public thành private để giá trị của nó không thể thay đổi bên ngoài lớp mà không tạo một đối tượng Employee mới. Sau đó, thêm phương thức static getHeadCount() trả về giá trị của thuộc tính static headcount
8. Abstract Class
Một abstract class (lớp trừu tượng) thường được sử dụng để định nghĩa hành vi chung cho các lớp dẫn xuất mở rộng. Không giống như một lớp thông thường, một lớp trừu tượng không thể được khởi tạo trực tiếp
Để khai báo một lớp trừu tượng, ta sử dụng từ khóa abstract
Thông thường, một abstract class chứa một hoặc nhiều phương thức trừu tượng (abstract method)
Một abstract method không chứa mã thực thi. Nó chỉ định nghĩa chữ ký của phương thức mà không bao gồm phần thân của phương thức. Một abstract method phải được triển khai trong lớp dẫn xuất
abstract class Employee {
constructor(private firstName: string, private lastName: string) {}
abstract getSalary(): number;
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
compensationStatement(): string {
return `${this.fullName} makes ${this.getSalary()} a month.`;
}
}
Ta không thể khởi tạo abstract class

Lớp FullTimeEmployee và Contractor kế thừa abstract class Employee:
class FullTimeEmployee extends Employee {
constructor(firstName: string, lastName: string, private salary: number) {
super(firstName, lastName);
}
getSalary(): number {
return this.salary;
}
}
class Contractor extends Employee {
constructor(
firstName: string,
lastName: string,
private rate: number,
private hours: number
) {
super(firstName, lastName);
}
getSalary(): number {
return this.rate * this.hours;
}
}
Thể hiện của 2 lớp FullTimeEmployee và Contractor
let tung = new FullTimeEmployee("Tung", "Hoang", 12000);
let minh = new Contractor("Quynh", "Nhu", 100, 200);
console.log(tung.compensationStatement());
// Tung Hoang makes 12000 a month.
console.log(minh.compensationStatement());
// Quynh Nhu makes 20000 a month.
9. Kết luận
Trên đây là những kiến thức cơ bản về Classes trong TypeScript. Hy vọng bài viết này sẽ giúp bạn hiểu và sử dụng được classes trong dự án của mình, và đồng thời, sẽ giúp bạn hiểu được lý do tại sao TypeScript được gọi là superset của JavaScript. Mình cũng chỉ mới tìm hiểu về TypeScript nên có lẽ bài viết sẽ không tránh khỏi sai sót, rất mong nhận được các ý kiến đóng góp từ bạn trong phần comments dưới bài viết để chúng ta cùng học thêm được những kiến thức hay ho nhé.
Cảm ơn bạn đã đọc bài viết của mình. Happy Coding 
10. Tham khảo
All rights reserved