Class trong TypeScript

Trong hướng dẫn này, bạn sẽ tìm hiểu về mọi thứ về class (lớp) trong TypeScript như: khai báo class, từ khóa kiểm soát truy cập, thuộc tính chỉ đọc (readonly property), kế thừa (inheritance), phương thức và thuộc tính tĩnh (static method and property), abstract class, ...

Giới thiệu về class trong TypeScript

JavaScript không có khái niệm về class (lớp) như các ngôn ngữ lập trình khác như Java, C#, PythonPHP. Trong ES5, bạn có thể sử dụng một hàm khởi tạo và kế thừa nguyên mẫu để tạo một "lớp".

Ví dụ: để tạo một lớp Person có ba thuộc tính ssn, firstName và lastName, bạn sử dụng hàm khởi tạo sau:

function Person(ssn, firstName, lastName) {
    this.ssn = ssn;
    this.firstName = firstName;
    this.lastName = lastName;
}

Tiếp theo, bạn có thể định nghĩa một phương thức nguyên mẫu (prototype method) để lấy tên đầy đủ của người bằng cách ghép tên và họ như sau:

Person.prototype.getFullName = function () {
    return `${this.firstName} ${this.lastName}`;
}

Sau đó, bạn có thể sử dụng "lớp" Person bằng cách tạo một đối tượng mới:

let person = new Person('171-28-0926','John','Doe');
console.log(person.getFullName());

Nó sẽ xuất những thông tin sau vào console:

John Doe

ES6 cho phép bạn định nghĩa một lớp chỉ đơn giản là đường cú pháp để tạo hàm khởi tạo và kế thừa nguyên mẫu:

class Person {
    ssn;
    firstName;
    lastName;

    constructor(ssn, firstName, lastName) {
        this.ssn = ssn;
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

Trong cú pháp khai báo lớp, hàm khởi tạo được định nghĩa rõ ràng và được đặt bên trong lớp. Đoạn mã sau đây thêm phương thức getFullName():

class Person {
    ssn;
    firstName;
    lastName;

    constructor(ssn, firstName, lastName) {
        this.ssn = ssn;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    getFullName() {
        return `${this.firstName} ${this.lastName}`;
    }
}

Sử dụng lớp Person cũng giống như hàm khởi tạo Person:

let person = new Person('171-28-0926','John','Doe');
console.log(person.getFullName());

Lớp (class) trong TypeScript 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. Phần sau cho thấy lớp Person trong TypeScript:

class Person {
    ssn: string;
    firstName: string;
    lastName: string;

    constructor(ssn: string, firstName: string, lastName: string) {
        this.ssn = ssn;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    getFullName(): string {
        return `${this.firstName} ${this.lastName}`;
    }
}

Khi bạn chú thích kiểu dữ liệu cho thuộc tính, hàm khởi tạo 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.

Ví dụ, bạn không thể khởi tạo cho thuộc tính ssn bằng một giá trị kiểu number. Đoạn mã sau sẽ dẫn đến lỗi:

let person = new Person(171280926, 'John', 'Doe');

Chỉ thị truy cập trong TypeScript

Các từ khóa 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 ba từ khóa chỉ thị truy cập:

  • private
  • protected
  • public
Lưu ý rằng TypeScript chỉ kiểm soát quyền truy cập lúc biên dịch, không phải trong lúc thực thi.

Chỉ thị truy cập private trong TypeScript

Chỉ thị truy cập private giới hạn khả năng truy cập trong cùng một lớp (class). Khi bạn thêm từ khóa private vào thuộc tính hoặc phương thức, bạn chỉ có thể truy cập thuộc tính hoặc phương thức đó trong cùng một lớp. Bất kỳ nỗ lực nào để truy cập các thuộc tính hoặc phương thức private bên ngoài lớp sẽ dẫn đến lỗi tại thời điểm biên dịch.

Dưới đây là ví dụ về cách sử dụng từ khóa private cho các thuộc tính snn, firstNamelastName của lớp person:

class Person {
    private ssn: string;
    private firstName: string;
    private lastName: string;
    // ...
}

Một khi các thuộc tính đã được khai báo từ khóa private, bạn chỉ có thể truy cập các thuộc tính này trong phương thức khởi tạo hoặc các phương thức khác của lớp Person. Ví dụ:

class Person {
    private ssn: string;
    private firstName: string;
    private lastName: string;

    constructor(ssn: string, firstName: string, lastName: string) {
        this.ssn = ssn;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    getFullName(): string {
        return `${this.firstName} ${this.lastName}`; 
    }
}

Các cố gắng truy cập thuộc tính ssn bên ngoài lớp Person sẽ bị lỗi khi biên dịch:

let person = new Person('153-07-3130', 'John', 'Doe');
console.log(person.ssn); // compile error

Chỉ thị truy cập public trong TypeScript

Chỉ thị truy cập public cho phép các thuộc tính và phương thức của lớp có thể truy cập được từ tất bất kỳ đâu. Nếu bạn không chỉ định bất kỳ chỉ thị truy cập nào cho các thuộc tính và phương thức, chúng sẽ sử dụng chỉ thị truy cập public theo mặc định.

Ví dụ, phương thức getFullName() của lớp Person có chỉ thị truy cập public:

class Person {
    // ...
    public getFullName(): string {
        return `${this.firstName} ${this.lastName}`; 
    }
    // ...
}

Nó tương tự như khi không khai báo từ khóa public.

Chỉ thị truy cập protected trong TypeScript

Chỉ thị truy cập protected cho phép các thuộc tính và phương thức của một lớp có thể truy cập được trong cùng một lớp và trong các lớp con.

Khi một lớp (lớp con) kế thừa từ một lớp khác (lớp cha), nó là một lớp con của lớp cha.

Trình biên dịch TypeScript sẽ gặp lỗi nếu bạn cố gắng truy cập các thuộc tính hoặc phương thức protected từ bất kỳ nơi nào khác.

Để thêm chỉ thị truy cập protected vào một thuộc tính hoặc một phương thức, bạn sử dụng từ khóa protected. Ví dụ:

class Person {

    protected ssn: string;
    
    // other code
}

Thuộc tính ssn bây giờ đã được bảo vệ. Nó chỉ có thể truy cập được trong lớp Person và trong bất kỳ lớp nào kế thừa từ lớp Person đó.

Lớp  Person có hai thuộc tính private và một thuộc tính protected. Hàm khởi tạo của nó khởi tạo các thuộc tính này thành ba đối số.

Để làm cho đoạn mã ngắn hơn, TypeScript cho phép bạn vừa khai báo các thuộc tính vừa khởi tạo chúng trong hàm khởi tạo như sau:

class Person {
    constructor(protected ssn: string, private firstName: string, private lastName: string) {
        this.ssn = ssn;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    getFullName(): string {
        return `${this.firstName} ${this.lastName}`;
    }
}

Khi bạn xem xét khả năng truy cập của các thuộc tính và phương thức, bạn nên bắt đầu với chỉ thị truy cập ít bị truy cập nhất, đó là private.

Getter và setter trong TypeScript

Giới thiệu về getter và setter trong TypeScript

Lớp Person đơn giản sau có ba thuộc tính: age, firstNamelastName:

class Person {
    public age: number;
    public firstName: string;
    public lastName: string;
}

Để truy cập bất kỳ thuộc tính nào của lớp Person, bạn chỉ cần làm như sau:

let person = new Person();
person.age = 26;

Giả sử rằng bạn gán một giá trị do người dùng nhập cho thuộc tính age như sau:

person.age = inputAge;

Giá trị của inputAge có thể là bất kỳ số nào. Để đảm bảo tính hợp lệ của tuổi, bạn có thể kiểm tra trước khi gán như sau:

if( inputAge > 0 && inputAge < 120 ) {
    person.age = inputAge;
}

Sử dụng đoạn mã kiểm tra này ở khắp mọi nơi thì dư thừa và tẻ nhạt.

Để tránh lặp lại việc kiểm tra, bạn có thể sử dụng setters và getters. Các getters và setters cho phép bạn kiểm soát quyền truy cập vào các thuộc tính của một lớp.

Đối với mỗi thuộc tính:

  • Phương thức getter trả về giá trị của thuộc tính. Một getter còn được gọi là một accessor.
  • Phương thức setter cậ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.

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 >= 120) {
            throw new Error('The age is invalid');
        }
        this._age = theAge;
    }

    public getFullName(): string {
        return `${this._firstName} ${this._lastName}`;
    }
}

Triển khai phương thức getter và setter như thế nào:

  • Trước hết, cập nhật chỉ thị truy cập của các thuộc tính age, firstNamelastName từ public thành private.
  • Thứ hai, thay đổi tên thuộc tính age thành _age.
  • Thứ ba, tạo phương thức getter và setter cho thuộc tính _age. Trong phương thức setter, hãy kiểm tra tính hợp lệ của độ tuổi đầu vào trước khi gán nó cho thuộc tính _age.

Bây giờ, bạn có thể truy cập phương thức setter của thuộc tính age như sau:

let person = new Person();
person.age = 10;
Lưu ý rằng lệnh gọi tới setter không có dấu ngoặc đơn như một phương thức thông thường. Khi bạn gọi person.age, phương thức setter của thuộc tính age được gọi.

Nếu bạn gán một giá trị không hợp lệ cho thuộc tính age, phương thức setter sẽ thông báo lỗi:

person.age = 0;

Lỗi:

Error: The age is invalid

Khi bạn truy cập person.age, phương thức getter của thuộc tính age được gọi.

console.log(person.age);

Phần sau thêm phương thức getter và setter vào các thuộc tính firstNamelastName.

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

Ví dụ về getter và setter trong TypeScript

Như bạn có thể thấy từ mã, setter rất hữu ích khi bạn muốn xác thực dữ liệu trước khi gán nó cho các thuộc tính. Ngoài ra, bạn có thể thực hiện các logic phức tạp khác.

Sau đây là cách tạo getter và setter cho thuộc tính fullName.

class Person {
    // ... other code 
    public get fullName() {
        return `${this.firstName} ${this.lastName}`;
    }

    public set fullName(name: string) {
        let parts = name.split(' ');
        if (parts.length != 2) {
            throw new Error('Invalid name format: first last');
        }
        this.firstName = parts[0];
        this.lastName = parts[1];
    }
}

Làm sao để tạo getter và setter cho thuộc tính fullName.

  • Phương thức getter trả về chuỗi được ghép nối của firstNamelastName.
  • Phương thức setter chấp nhận một chuỗi là tên đầy đủ với định dạng: first last và gán phần đầu tiên cho thuộc tính firstName và phần thứ hai cho thuộc tính lastName.

Bây giờ, bạn có thể truy cập setter và getter của thuộc tính fullname giống như một thuộc tính lớp thông thường:

let person = new Person();
person.fullName = 'John Doe';

console.log(person.fullName);

Kế thừa (inheritance) trong TypeScript

Giới thiệu về kế thừa 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 được gọi là kế thừa (inheritance) trong TypeScript.

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 được gọi là lớp cha. Những cái tên này xuất phát từ bản chất con cái được thừa hưởng gen từ cha mẹ.

Kế thừa cho phép bạn 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 nó.

JavaScript sử dụng kế thừa nguyên mẫu (prototype inheritance), không phải kế thừa cổ điển như Java hoặc C#. ES6 giới thiệu cú pháp class đơn giản là cú pháp của kế thừa nguyên mẫu. TypeScript hỗ trợ kế thừa như ES6.

Giả sử bạn có lớp Person sau :

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}.`;
    }
}

Để kế thừa một lớp, bạn sử dụng từ khóa extends. Ví dụ, lớp Employee sau kế thừa lớp Person:

class Employee extends Person {
    //..
}

Trong ví dụ này, Employee là một lớp con và Person là lớp cha.

Phương thức khởi tạo (constructor)

Vì lớp Person có một phương thức khởi tạo các thuộc tính firstNamelastName, bạn cần 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 của 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, bạn sử dụng cú pháp super(). Ví dụ:

class Employee extends Person {
    constructor(firstName: string, lastName: string, private jobTitle: string) { 
        // call the constructor of the Person class:
        super(firstName, lastName);
    }
}

Sau đây tạo một thể hiện của lớp Employee:

let employee = new Employee('John','Doe','Front-end Developer');

Vì lớp Employee kế thừa các thuộc tính và phương thức của lớp Person, bạn có thể gọi các phương thức getFullName()describe() trên đối tượng employee như sau:

let employee = new Employee('John', 'Doe', 'Web Developer');

console.log(employee.getFullName());
console.log(employee.describe());

Đầu ra:

John Doe
This is John Doe.

Ghi đè phương thức (method overriding)

Khi bạn gọi phương thức employee.describe() trên đối tượng employee, phương thức describe() của lớp Person được thực thi và hiển thị thông điệp: This is John Doe.

Nếu bạn muốn lớp Employee có phiên bản riêng của phương thức describe(), bạn có thể định nghĩa nó 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(), chúng ta gọi phương thức describe() của lớp cha bằng cú pháp super.methodInParentClass().

Nếu bạn gọi phương thức describe() trên đối tượng employee, phương thức describe() trong lớp Employee được gọi:

let employee = new Employee('John', 'Doe', 'Web Developer');
console.log(employee.describe());

Đầu ra:

This is John Doe.I'm a Web Developer.

Phương thức và thuộc tính static trong TypeScript

Thuộc tính static trong TypeScript

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 một thuộc tính tĩnh, bạn sử dụng từ khóa static. Để truy cập thuộc tính tĩnh, bạn sử dụng cú pháp className.propertyName. Ví dụ:

class Employee {
    static headcount: number = 0;

    constructor(
        private firstName: string,
        private lastName: string, 
        private jobTitle: string) {
        Employee.headcount++;
    }
}

Trong ví dụ này, thuộc tính tĩnh headcount được khởi tạo bằng 0. Giá trị của nó được tăng lên 1 bất cứ khi nào một đối tượng mới được tạo.

Phần sau tạo hai đối tượng Employee và hiển thị giá trị của thuộc tính headcount. Nó trả về hai như mong đợi.

let john = new Employee('John', 'Doe', 'Front-end Developer');
let jane = new Employee('Jane', 'Doe', 'Back-end Developer');

console.log(Employee.headcount); // 2

Phương thức static trong TypeScript

Tương tự như thuộc tính static, một phương thức static cũng được chia sẻ trên các thể hiện của lớp. Để khai báo một phương thức tĩnh, bạn sử dụng từ khóa static trước tên phương thức. Ví dụ:

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

Trong ví dụ này:

  • Đầu tiên, 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 .
  • Thứ hai, thêm phương thức static getHeadcount() trả về giá trị của thuộc tính static headcount.

Để gọi một phương thức tĩnh, bạn sử dụng cú pháp className.staticMethod(). Ví dụ:

let john = new Employee('John', 'Doe', 'Front-end Developer');
let jane = new Employee('Jane', 'Doe', 'Back-end Developer');

console.log(Employee.getHeadcount); // 2

Trong thực tế, bạn sẽ tìm thấy thư viện chứa nhiều thuộc tính và phương thức tĩnh giống như đối tượng Math. Nó có các thuộc tính tĩnh PI, E... và các phương pháp tĩnh abs(), round(), vv.

Abstract class trong TypeScript

Giới thiệu về abstract class trong TypeScript

Một abstract class thường được sử dụng để định nghĩa các 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, bạn sử dụng từ khóa abstract:

abstract class Employee {
    //...
}

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.

Phần sau cho thấy abstract class Employee có abstract method getSalary():

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.`;
    }
}

Trong lớp Employee:

  • Hàm khởi tạo khai báo các thuộc tính firstNamelastName.
  • Các phương thức getSalary() là một phương thức trừu tượng (abstract method). Lớp dẫn xuất sẽ thực hiện logic dựa trên lớp Employee.
  • Các phương thức getFullName()compensationStatement() chứa mã thực thi. Lưu ý rằng phương thức compensationStatement() gọi phương thức trừu tượng getSalary().

Vì lớp Employee là abstract class, bạn không thể khởi tạo một đối tượng mới từ nó. Câu lệnh sau gây ra lỗi:

let employee = new Employee('John','Doe');

Lỗi:

error TS2511: Cannot create an instance of an abstract class.

Lớp FullTimeEmployee sau kế thừa từ lớp Employee:

class FullTimeEmployee extends Employee {
    constructor(firstName: string, lastName: string, private salary: number) {
        super(firstName, lastName);
    }
    getSalary(): number {
        return this.salary;
    }
}

Trong lớp FullTimeEmployee này, thuộc tính salary được đặt trong hàm khởi tạo. Vì getSalary() là một phương thức trừu tượng của lớp Employee, nên lớp FullTimeEmployee cần triển khai phương thức này. Trong ví dụ này, nó chỉ trả về thuộc tính salary mà không cần tính toán.

Phần sau cho thấy lớp Contractor cũng kế thừa từ lớp Employee:

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

Trong lớp Contractor, hàm khởi tạo có thuộc tính ratehours. Phương thức getSalary() tính toán mức lương bằng cách nhân rate với hours.

Đoạn mã sau đây tạo một đối tượng của FullTimeEmployeeContractor, sau đó in ra console:

let john = new FullTimeEmployee('John', 'Doe', 12000);
let jane = new Contractor('Jane', 'Doe', 100, 160);

console.log(john.compensationStatement());
console.log(jane.compensationStatement());

Đầu ra:

John Doe makes 12000 a month.
Jane Doe makes 16000 a month.

Bạn nên sử dụng các abstract class khi muốn chia sẻ mã giữa một số lớp có liên quan.

Tóm lược

  • Sử dụng từ khóa class để định nghĩa một lớp trong TypeScript.
  • TypeScript tận dụng cú pháp class của ES6 và thêm các chú thích kiểu dữ liệu để làm cho lớp mạnh mẽ hơn.
  • TypeScript cung cấp ba chỉ thị truy cập vào thuộc tính và phương thức của lớp là: private, protectedpublic.
  • Chỉ thị truy cập private chỉ cho phép truy cập các thuộc tính và phương thức trong cùng một lớp.
  • Chỉ thị truy cập protected chỉ cho phép truy cập các thuộc tính và phương thức trong cùng một lớp và các lớp con.
  • Chỉ thị truy cập public cho phép truy cập các thuộc tính và phương thức từ bất kỳ vị trí nào.
  • Sử dụng phương thức getters / setters trong TypeScript để kiểm soát truy cập các thuộc tính của một lớp.
  • Getter / setters còn được gọi là accessor / mutator.
  • Sử dụng từ khóa extends để cho phép một lớp kế thừa từ một lớp khác.
  • Sử dụng super() trong hàm khởi tạo của lớp con để gọi hàm khởi tạo của lớp cha. Ngoài ra, sử dụng cú pháp super.methodInParentClass() để gọi phương thức methodInParentClass() của lớp cha.
  • Các thuộc tính và phương thức static được chia sẻ bởi tất cả các thể hiện của một lớp.
  • Sử dụng từ khóa static trước một thuộc tính hoặc một phương thức để làm cho nó static.
  • Các lớp trừu tượng (abstract class) không thể khởi tạo thể hiện.
  • Một abstract class có ít nhất một phương thức trừu tượng (abstract method).
  • Để sử dụng một abstract class, bạn cần kế thừa nó và cung cấp cách triển khai cho các abstract method.
TypeScript
Bài Viết Liên Quan:
Generic type trong TypeScript
Trung Nguyen 20/12/2020
Generic type trong TypeScript

Trong hướng dẫn này, bạn sẽ tìm hiểu về generic type trong TypeScript cho phép bạn sử dụng các kiểu dữ liệu làm tham số chính thức.

Kiểu dữ liệu nâng cao trong TypeScript
Trung Nguyen 08/12/2020
Kiểu dữ liệu nâng cao trong TypeScript

Trong hướng dẫn này, bạn sẽ tìm hiểu về kiểu giao (intersection type), an toàn kiểu (type guard), ép kiểu (type casting) và xác nhận kiểu (type assertions) trong TypeScript .

Interface trong TypeScript
Trung Nguyen 06/12/2020
Interface trong TypeScript

Trong hướng dẫn này, bạn sẽ tìm hiểu về interface trong TypeScript, cách định nghĩa, cách mở rộng interface và cách sử dụng chúng để thực thi kiểm tra kiểu dữ liệu.

Function trong TypeScript
Trung Nguyen 03/12/2020
Function trong TypeScript

Trong hướng dẫn này, bạn sẽ tìm hiểu về function (hàm) trong TypeScript và cách sử dụng chú thích kiểu để thực thi kiểm tra kiểu dữ liệu cho function.