Một lớp TypeScript kế thừa (inheritance) lớp khác bằng extends. Lớp con nhận mọi field và phương thức của lớp cha, gọi hàm khởi tạo của lớp cha bằng super(...) và có thể ghi đè phương thức, còn TypeScript kiểm tra để mỗi phương thức ghi đè vẫn tương thích với kiểu của lớp cha.
Đây là kế thừa lớp của JavaScript (xem kế thừa trong JavaScript để hiểu prototype chain phía sau). TypeScript thêm phần kiểm tra kiểu: một Dog gán được vào bất cứ chỗ nào cần một Animal, và trình biên dịch xác minh phương thức ghi đè lẫn lời gọi super.
Ghi đè phương thức
Một phương thức của lớp con có cùng tên sẽ thay thế phương thức của lớp cha. Kiểu của nó phải gán được cho kiểu của lớp cha: tham số giống hoặc tương thích, kiểu trả về giống hoặc hẹp hơn. Nếu không, bất kỳ đoạn code nào đang giữ một Animal đều có thể gọi phương thức với những đối số mà lớp con không xử lý được.
Đổi tham số thành value: string sẽ gây lỗi TS2416: Property 'format' in type 'CurrencyFormatter' is not assignable to the same property in base type 'Formatter'. Vòng lặp cho thấy lý do: nó gọi format với một số trên mọi phần tử mà không biết mình đang có lớp con nào.
Có một lỗ hổng. Tham số của phương thức được so sánh theo kiểu bivariant, nên một phương thức ghi đè nhận kiểu hẹp hơn, như format(value: 1 | 2), vẫn biên dịch được dù vòng lặp vẫn có thể truyền 1234567 vào. Kiểu trả về thì được kiểm tra chặt: trả về number thay vì string là lỗi TS2416.
Từ khóa override và noImplicitOverride
override nói rằng "thành viên này thay thế một thành viên trong lớp cơ sở", và trình biên dịch bắt bạn giữ lời. Nếu lớp cơ sở không có thành viên đó (gõ sai, hoặc một phương thức ai đó đã đổi tên ở lớp cha), đó là lỗi:
index.ts(9,14): error TS4117: This member cannot have an 'override' modifier because it is not declared in the base class 'Animal'. Did you mean 'speak'?
Không có override, lỗi gõ đó vẫn biên dịch được và speak() lặng lẽ trả về "...". Từ khóa chỉ có tác dụng ở nơi nó được viết, nên hãy bật noImplicitOverride trong tsconfig.json. Khi đó mọi phương thức ghi đè một thành viên của lớp cơ sở đều phải nói rõ điều đó, và một speak() thông thường trong lớp con là lỗi TS4114 (This member must have an 'override' modifier because it overrides a member in the base class 'Animal').
{
"compilerOptions": {
"strict": true,
"noImplicitOverride": true
}
}
noImplicitOverride không nằm trong strict; bạn phải tự thêm nó. override bị xóa khỏi JavaScript sau khi biên dịch.
Thành viên protected trong lớp con
Các thành viên private của lớp cha không nhìn thấy được trong lớp con. Các thành viên protected thì nhìn thấy được, và đó chính là lý do protected tồn tại: nó là API mà lớp cha cung cấp cho các lớp con.
Xem access modifier để biết đầy đủ quy tắc, kể cả lý do cả private lẫn protected đều không được thực thi lúc chạy.
Khai báo lại field với kiểu hẹp hơn
Lớp con thường biết một field chứa thứ cụ thể hơn những gì lớp cha nói. Khai báo lại nó thành pet: Dog có tác động lúc chạy với class field hiện đại: field của lớp con được khởi tạo thành undefined sau khi super() trả về, xóa mất thứ mà hàm khởi tạo của lớp cha đã lưu. Trừ khi hàm khởi tạo của lớp con gán lại, TypeScript báo điều này là TS2612 (Property 'pet' will overwrite the base property in 'Home'). Dùng declare để chỉ đổi kiểu:
declare không sinh ra gì cả, nên giá trị do hàm khởi tạo của Home gán vẫn còn nguyên. Chính hàm khởi tạo chỉ nhận Dog là thứ làm cho kiểu hẹp hơn đó đúng.
implements và extends
Hai từ khóa trả lời hai câu hỏi khác nhau, và một lớp có thể dùng cả hai.
extends | implements | |
|---|---|---|
| Dùng với | một lớp | bao nhiêu interface cũng được (hoặc type alias của kiểu object) |
| Mang theo code | có: field, phương thức, hàm khởi tạo | không, chỉ kiểm tra hình dạng |
| Tồn tại lúc chạy | có, qua prototype chain | không, bị xóa |
instanceof hoạt động | có | không |
| Cách dùng điển hình | chia sẻ hành vi thật | cam kết một hình dạng với phần còn lại của code |
Một lớp chỉ có thể kế thừa một lớp (TS1174: Classes can only extend a single class). Để kết hợp hành vi từ nhiều nơi, hãy implement nhiều interface và chia sẻ code qua composition (một field giữ một object trợ giúp) hoặc mixin.
Mixin
Mixin là một hàm nhận một lớp và trả về một lớp con của nó. Nó cho bạn một dạng đa kế thừa vẫn được kiểm tra kiểu:
any[] trong Constructor là bắt buộc: TypeScript chỉ chấp nhận lớp cơ sở của mixin khi hàm khởi tạo của nó nhận đúng một rest parameter kiểu any[]. Mixin dùng được nhưng làm kiểu và stack trace khó đọc hơn, nên hãy ưu tiên composition trước.
Lỗi thường gặp
- Gọi một phương thức có thể bị ghi đè từ hàm khởi tạo của lớp cơ sở. Hàm khởi tạo của lớp cơ sở chạy trước khi các field của lớp con được khởi tạo, nên một phương thức ghi đè đọc
this.itemssẽ thấyundefined, và một lời gọi nhưthis.items.push(x)ném raTypeErrorlúc chạy. TypeScript không bắt được lỗi này. - Quên
super(...)hoặc gọi nó quá muộn. LỗiTS2377vàTS17009; lớp cha phải dựng xong object trước khi lớp con đụng tớithis. - Khai báo lại field của lớp cha mà không có
declare. Field của lớp con đặt lại nó thànhundefined. - Phân cấp quá sâu. Mỗi cấp lại buộc lớp con phụ thuộc vào chi tiết bên trong của lớp cha. Hai cấp thường là đủ; vượt quá đó, hãy ưu tiên composition hoặc interface.
Câu hỏi thường gặp
Kế thừa hoạt động thế nào trong TypeScript?
Một lớp kế thừa lớp khác bằng extends: class Dog extends Animal. Lớp con nhận các field và phương thức của lớp cha, gọi hàm khởi tạo của lớp cha bằng super(...) và có thể ghi đè phương thức. TypeScript kiểm tra để các phương thức ghi đè luôn tương thích kiểu với lớp cha.
Từ khóa override trong TypeScript dùng để làm gì?
override đánh dấu một phương thức hoặc thuộc tính thay thế một thành viên của lớp cơ sở. Nếu lớp cơ sở không có thành viên nào trùng tên, trình biên dịch báo TS4117, nhờ vậy bắt được lỗi gõ sai và những thành viên đã bị đổi tên ở lớp cha. Khi bật noImplicitOverride, mọi phương thức ghi đè đều phải có từ khóa này (nếu không sẽ báo TS4114).
Làm sao gọi phương thức của lớp cha trong TypeScript?
Dùng super.methodName(...) bên trong phương thức của lớp con. Trong hàm khởi tạo, super(...) gọi hàm khởi tạo của lớp cha và phải chạy trước mọi lần dùng this.
TypeScript có hỗ trợ đa kế thừa không?
Không. Một lớp chỉ có thể kế thừa một lớp (TS1174: Classes can only extend a single class). Nó có thể implement bao nhiêu interface cũng được, và mixin (hàm nhận một lớp rồi trả về một lớp con) giúp kết hợp hành vi từ nhiều nguồn.
extends và implements khác nhau thế nào?
extends kế thừa code thật từ một lớp cha. implements chỉ kiểm tra rằng lớp khớp với hình dạng của một interface và không thêm gì lúc chạy. Một lớp có thể dùng cả hai: class Dog extends Animal implements Pet.