Menu

Class trong TypeScript: thuộc tính, phương thức, implements

Class trong TypeScript là class JavaScript có thêm kiểu cho thuộc tính, phương thức và hàm khởi tạo. Tìm hiểu cách khai báo thuộc tính và strictPropertyInitialization hoạt động, cách gán kiểu cho this, getter và setter, thành viên static, implements, và vì sao một class cũng là một kiểu.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Class trong TypeScript là một class JavaScript có thêm kiểu: mỗi thuộc tính (field) được khai báo kèm kiểu trong thân class, còn phương thức và hàm khởi tạo (constructor) có tham số và giá trị trả về được gán kiểu.

Kết quả biên dịch là một class JavaScript bình thường đã bỏ hết kiểu, nên mọi điều về class trong JavaScript vẫn đúng. Phần TypeScript thêm vào diễn ra lúc biên dịch: lời gọi cuối cùng bị từ chối trước khi chương trình chạy.

Khai báo thuộc tính

Một thuộc tính phải được khai báo trong thân class thì hàm khởi tạo mới gán được. Gán this.owner mà chưa khai báo owner là lỗi TS2339 (thuộc tính không tồn tại). Thuộc tính có giá trị khởi tạo sẽ được suy ra kiểu từ giá trị đó, giống như một biến.

Khai báoÝ nghĩa
name: string;Phải được gán trong hàm khởi tạo
count = 0;Đã khởi tạo, kiểu được suy ra là number
label?: string;Tùy chọn, kiểu là string | undefined
readonly id: number;Gán một lần, sau đó chỉ đọc
data!: string[];Definite assignment: cứ yên tâm, nó sẽ được gán
static total = 0;Thuộc về class, không thuộc về instance

Khi bật strict, strictPropertyInitialization kiểm tra rằng mọi thuộc tính không tùy chọn đều có giá trị. Class này quên mất hai thuộc tính:

Trình biên dịch báo cả hai:

index.ts(3,5): error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor.
index.ts(4,5): error TS2564: Property 'age' has no initializer and is not definitely assigned in the constructor.

Phép kiểm tra này tồn tại vì nếu không có nó, new Profile().name.toUpperCase() sẽ qua được bước kiểm tra kiểu rồi sập vì undefined. Bạn sửa từng thuộc tính theo một trong bốn cách: cho nó giá trị khởi tạo, gán nó trong hàm khởi tạo, đánh dấu tùy chọn bằng ?, hoặc viết name!: string khi một framework hay một phương thức init() sẽ gán nó sau. Cách cuối cùng tắt phép kiểm tra cho thuộc tính đó, nên chỉ dùng khi thật cần.

Phương thức và this

Phương thức được gán kiểu giống như hàm. Bên trong phương thức, this có kiểu của instance. Một phương thức có thể trả về this để cho phép gọi nối tiếp (chaining), và kiểu trả về tự động theo các class con.

class QueryBuilder {
    private parts: string[] = [];

    where(condition: string): this {
        this.parts.push(condition);
        return this;
    }

    build(): string {
        return this.parts.join(" AND ");
    }
}

const sql = new QueryBuilder().where("age > 18").where("active = 1").build();
console.log(sql); // age > 18 AND active = 1

Một phương thức bị mất this khi bạn tách nó ra khỏi đối tượng, một bug kinh điển của JavaScript. Mặc định TypeScript không bắt được lỗi này, nhưng sẽ bắt được nếu phương thức khai báo tham số this. Tham số đó bị xóa khỏi kết quả biên dịch và chỉ dùng để canh chỗ gọi hàm:

Thuộc tính dạng arrow function hoạt động được vì mỗi instance có một hàm riêng giữ lại this qua closure. Cái giá phải trả là mỗi instance một hàm, thay vì một phương thức dùng chung trên prototype.

Getter và setter

Accessor get và set nhìn từ bên ngoài giống như thuộc tính. Một getter không có setter là chỉ đọc: gán giá trị cho nó là lỗi biên dịch (TS2540).

Trình biên dịch từ chối phép gán, và nếu phép kiểm tra bị bỏ qua thì khi chạy cũng ném ra TypeError, vì tệp đã biên dịch chạy ở strict mode (trình biên dịch thêm "use strict" ở đầu tệp). RangeError trong setter là phép kiểm tra lúc chạy do chính bạn viết. Kiểu number chỉ nói rằng giá trị là một số; nó không biết gì về khoảng giá trị.

Thành viên static

Thuộc tính và phương thức static thuộc về chính class. Bạn truy cập chúng qua tên class, không qua instance. Khối static { } chạy một lần khi class được định nghĩa.

Thành viên static không dùng được tham số kiểu của class: trong class Box<T>, static empty: T là lỗi TS2302, vì chỉ có một thuộc tính static duy nhất, dùng chung cho mọi Box<string>, Box<number> và các kiểu khác.

Hiện thực một interface

implements yêu cầu trình biên dịch kiểm tra rằng class có đủ mọi thứ mà interface đòi hỏi. Thiếu một thành viên là lỗi TS2420: Class 'X' incorrectly implements interface 'Y'.

Một class có thể implements nhiều interface: class Doc implements Printable, Serializable. Có hai việc implements không làm. Nó không tự gán kiểu cho tham số của phương thức: check(s) { ... } trong một class implements check(s: string): boolean vẫn là lỗi TS7006 (Parameter 's' implicitly has an 'any' type), nên bạn vẫn phải chú thích kiểu. Và nó không để lại dấu vết nào khi chạy, nên không thể viết obj instanceof Shape.

private radius trong hàm khởi tạo là một parameter property: nó khai báo và gán thuộc tính trong một bước. Xem hàm khởi tạo.

Class cũng là một kiểu

Khai báo class tạo ra hai thứ cùng một tên: một giá trị (hàm khởi tạo bạn gọi bằng new) và một kiểu (hình dạng của một instance). Vì TypeScript so sánh kiểu theo cấu trúc, mọi đối tượng có cùng các thành viên public đều khớp với kiểu đó, kể cả đối tượng không được tạo ra từ class.

Đối tượng thường qua được bước kiểm tra kiểu nhưng khi chạy thì không phải là Point: ({ x: 6, y: 8 }) instanceof Point là false. Một class có thành viên private hoặc #private sẽ chặn điều này: chỉ instance của class đó (hoặc của class con) mới gán được cho nó.

Lỗi thường gặp

  • Khai báo thuộc tính mà không bao giờ gán. TS2564 đang báo rằng thuộc tính sẽ là undefined. Hãy khởi tạo nó thay vì dập lỗi bằng !.
  • Truyền phương thức làm callback. button.onclick = obj.handle làm mất this. Dùng thuộc tính dạng arrow function hoặc obj.handle.bind(obj).
  • Tưởng implements thêm code hoặc kiểu. Nó chỉ kiểm tra. Kiểu của tham số vẫn cần chú thích.
  • Dùng instanceof với interface. Interface không tồn tại khi chạy; hãy kiểm tra một class hoặc dùng type guard.
  • Tưởng kiểu sẽ kiểm tra dữ liệu. Thuộc tính number nhận mọi số lúc biên dịch, và nhận bất cứ thứ gì từ dữ liệu không có kiểu lúc chạy.

Câu hỏi thường gặp

Làm sao để tạo một class trong TypeScript?

Khai báo từng thuộc tính cùng kiểu của nó trong thân class, sau đó viết hàm khởi tạo và các phương thức như trong JavaScript: class User { name: string; constructor(name: string) { this.name = name; } greet(): string { return "Hi, " + this.name; } }. Tạo instance bằng new User("Ada").

Lỗi "has no initializer and is not definitely assigned in the constructor" nghĩa là gì?

Đây là lỗi TS2564 do strictPropertyInitialization (một phần của strict) báo. Một thuộc tính kiểu string sẽ có giá trị ban đầu là undefined vì không có gì gán cho nó. Bạn sửa bằng cách cho nó giá trị khởi tạo (name = ""), gán nó trong hàm khởi tạo, khai báo nó là tùy chọn (name?: string), hoặc, khi có thứ gì đó bên ngoài hàm khởi tạo gán nó, dùng definite assignment assertion (name!: string).

implements và extends trong TypeScript khác nhau thế nào?

extends kế thừa code từ class cha: các thuộc tính và phương thức của class cha thật sự có mặt ở class con khi chạy. implements chỉ yêu cầu trình biên dịch kiểm tra rằng class có đúng hình dạng của một interface. Nó không thêm gì vào class và biến mất khỏi JavaScript sau khi biên dịch.

Có thể dùng class làm kiểu trong TypeScript không?

Có. Tên class vừa là một giá trị (hàm khởi tạo) vừa là một kiểu (hình dạng của các instance). let u: User nhận mọi đối tượng có cùng các thành viên public, vì TypeScript so sánh kiểu theo cấu trúc. typeof User là kiểu của chính hàm khởi tạo.

Class TypeScript có khác class JavaScript khi chạy không?

Không. TypeScript biên dịch class thành một class JavaScript bình thường. Chú thích kiểu, mệnh đề implements và các modifier như private đều bị xóa; chỉ những tính năng của JavaScript như thuộc tính #private, khối static và getter mới tồn tại khi chạy.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu