Пошук уроків, статей та іншого контенту
Наслідуйте класи через extends, реалізовуйте контракти через implements і створюйте абстрактні базові класи.
extendsНаслідування дає змогу створити новий клас на основі вже наявного. Дочірній клас отримує властивості й методи батьківського класу та може:
використовувати їх без повторного оголошення;
додавати власну поведінку;
змінювати реалізацію успадкованих методів;
викликати конструктор батьківського класу через super.
Для наслідування в TypeScript використовується ключове слово extends.
class User {
constructor(
public readonly name: string,
protected email: string
) {}
describe(): string {
return `${this.name} користується системою`;
}
changeEmail(newEmail: string): void {
this.email = newEmail;
}
}
class Admin extends User {
constructor(
name: string,
email: string,
private readonly permissions: string[]
) {
super(name, email);
}
hasPermission(permission: string): boolean {
return this.permissions.includes(permission);
}
describe(): string {
return `${this.name} має права адміністратора`;
}
}
const admin = new Admin("Олена", "olena@example.com", [
"users:read",
"users:delete",
]);
console.log(admin.describe());
console.log(admin.hasPermission("users:read"));
admin.changeEmail("new-email@example.com");Конструктор Admin викликає super(name, email). Це обов’язково, якщо дочірній клас має власний конструктор. Виклик super створює та ініціалізує частину об’єкта, яка належить класу User.
Дочірній клас може надати власну реалізацію успадкованого методу. Це називається перевизначенням методу.
У прикладі Admin перевизначає метод describe():
class User {
constructor(public readonly name: string) {}
describe(): string {
return `Користувач: ${this.name}`;
}
}
class Admin extends User {
describe(): string {
return `Адміністратор: ${this.name}`;
}
}
const users: User[] = [
new User("Іван"),
new Admin("Олена"),
];
for (const user of users) {
console.log(user.describe());
}Змінна може мати тип User, але під час виконання буде викликана реалізація методу відповідного фактичного об’єкта. Це називається поліморфізмом.
Щоб явно позначити, що метод перевизначає метод батьківського класу, можна використовувати override:
class Admin extends User {
override describe(): string {
return `Адміністратор: ${this.name}`;
}
}Модифікатор override допомагає виявити помилки. Наприклад, TypeScript повідомить про помилку, якщо в батьківському класі немає методу з такою назвою. Для суворішого контролю можна увімкнути опцію noImplicitOverride.
Через super можна викликати метод батьківського класу:
class User {
constructor(public readonly name: string) {}
describe(): string {
return `Користувач: ${this.name}`;
}
}
class Admin extends User {
override describe(): string {
return `${super.describe()} з правами адміністратора`;
}
}
console.log(new Admin("Олена").describe());Результат:
Користувач: Олена з правами адміністратораimplementsimplements використовується, коли клас має відповідати певному контракту. Таким контрактом найчастіше є інтерфейс.
Інтерфейс описує, які властивості та методи повинен мати клас, але не містить їх реалізації:
interface Serializable {
serialize(): string;
}
class Product implements Serializable {
constructor(
public readonly id: number,
public readonly name: string,
public readonly price: number
) {}
serialize(): string {
return JSON.stringify({
id: this.id,
name: this.name,
price: this.price,
});
}
}
const product = new Product(1, "Клавіатура", 2500);
console.log(product.serialize());Якщо клас не реалізує всі вимоги інтерфейсу, TypeScript повідомить про помилку:
interface Serializable {
serialize(): string;
}
class Product implements Serializable {
// Помилка: відсутній метод serialize
}implements перевіряє структуру класу під час компіляції. Сам інтерфейс не створюється під час виконання JavaScript і не додає класу готових методів.
Клас може реалізовувати кілька інтерфейсів:
interface Serializable {
serialize(): string;
}
interface Loggable {
getLogMessage(): string;
}
class Order implements Serializable, Loggable {
constructor(
public readonly id: number,
public readonly total: number
) {}
serialize(): string {
return JSON.stringify({
id: this.id,
total: this.total,
});
}
getLogMessage(): string {
return `Замовлення #${this.id}, сума: ${this.total}`;
}
}
const order = new Order(42, 1999);
console.log(order.serialize());
console.log(order.getLogMessage());Це корисно, коли один клас повинен підтримувати кілька незалежних можливостей.
extends та implementsКлас може одночасно:
успадковувати реалізацію від іншого класу через extends;
відповідати одному або кільком контрактам через implements.
interface Cacheable {
getCacheKey(): string;
}
class Entity {
constructor(public readonly id: number) {}
}
class Article extends Entity implements Cacheable {
constructor(
id: number,
public readonly title: string
) {
super(id);
}
getCacheKey(): string {
return `article:${this.id}`;
}
}
const article = new Article(10, "Вступ до TypeScript");
console.log(article.id);
console.log(article.title);
console.log(article.getCacheKey());У TypeScript спочатку вказують батьківський клас, а потім інтерфейси:
class Child extends Parent implements FirstContract, SecondContract {
// ...
}Клас може успадковуватися лише від одного класу, але реалізовувати багато інтерфейсів.
Абстрактний клас — це базовий клас, призначений для наслідування. Його не можна безпосередньо створити через new.
Для оголошення використовується ключове слово abstract:
abstract class Payment {
constructor(
protected readonly amount: number
) {}
abstract process(): void;
printReceipt(): void {
console.log(`Сума платежу: ${this.amount}`);
}
}Метод process() оголошений як абстрактний. Він не має реалізації в базовому класі, тому кожен неабстрактний дочірній клас повинен реалізувати його.
class CardPayment extends Payment {
override process(): void {
console.log(`Оплата карткою на суму ${this.amount}`);
}
}
class CashPayment extends Payment {
override process(): void {
console.log(`Оплата готівкою на суму ${this.amount}`);
}
}
const payments: Payment[] = [
new CardPayment(1200),
new CashPayment(800),
];
for (const payment of payments) {
payment.process();
payment.printReceipt();
}Результат:
Оплата карткою на суму 1200
Сума платежу: 1200
Оплата готівкою на суму 800
Сума платежу: 800Спроба створити екземпляр абстрактного класу є помилкою:
abstract class Payment {
abstract process(): void;
}
// Помилка компіляції:
// Cannot create an instance of an abstract class.
const payment = new Payment();Абстрактними можуть бути не лише методи, а й властивості:
abstract class Report {
abstract readonly format: string;
abstract generate(): string;
print(): void {
console.log(`[${this.format}] ${this.generate()}`);
}
}
class JsonReport extends Report {
readonly format = "JSON";
generate(): string {
return JSON.stringify({ status: "ok" });
}
}
class TextReport extends Report {
readonly format = "TEXT";
generate(): string {
return "status: ok";
}
}
const reports: Report[] = [
new JsonReport(),
new TextReport(),
];
for (const report of reports) {
report.print();
}Абстрактний клас може містити одночасно:
звичайні властивості;
звичайні методи з готовою реалізацією;
абстрактні властивості;
абстрактні методи;
конструктор.
Це відрізняє абстрактний клас від інтерфейсу: абстрактний клас може повторно використовувати спільну реалізацію.
abstract class чи interfaceОбидва механізми дають змогу описувати вимоги до класів, але використовуються по-різному.
Використовуйте інтерфейс, коли потрібно описати контракт:
interface HasId {
id: number;
}
interface CanArchive {
archive(): void;
}Інтерфейс не визначає, як саме працюють методи. Він лише перевіряє, що необхідні властивості та методи існують.
Використовуйте абстрактний клас, коли дочірні класи мають спільний стан або спільну реалізацію:
abstract class Document {
constructor(
protected readonly title: string
) {}
printTitle(): void {
console.log(this.title);
}
abstract export(): string;
}Дочірні класи отримають метод printTitle(), але кожен самостійно реалізує export().
Клас може успадковуватися лише від одного класу через extends.
Клас може реалізовувати багато інтерфейсів через implements.
Абстрактний клас може містити реалізацію.
Інтерфейс задає форму об’єкта або контракт класу.
Абстрактний клас не можна створити безпосередньо.
Інтерфейс існує лише на етапі перевірки TypeScript і не створюється під час виконання.
У наступному прикладі поєднано всі розглянуті можливості:
interface Trackable {
getTrackingInfo(): string;
}
abstract class Delivery implements Trackable {
constructor(
public readonly id: number,
protected readonly address: string
) {}
abstract calculatePrice(): number;
abstract getTrackingInfo(): string;
printSummary(): void {
console.log(
`Доставка #${this.id}, адреса: ${this.address}, ` +
`вартість: ${this.calculatePrice()}`
);
}
}
class CourierDelivery extends Delivery {
constructor(
id: number,
address: string,
private readonly distance: number
) {
super(id, address);
}
override calculatePrice(): number {
return 100 + this.distance * 20;
}
override getTrackingInfo(): string {
return `Кур'єрська доставка #${this.id}`;
}
}
class PickupDelivery extends Delivery {
constructor(
id: number,
address: string,
private readonly storeName: string
) {
super(id, address);
}
override calculatePrice(): number {
return 0;
}
override getTrackingInfo(): string {
return `Самовивіз із магазину "${this.storeName}"`;
}
}
const deliveries: Delivery[] = [
new CourierDelivery(1, "вул. Шевченка, 10", 5),
new PickupDelivery(2, "вул. Франка, 20", "Центральний"),
];
for (const delivery of deliveries) {
delivery.printSummary();
console.log(delivery.getTrackingInfo());
}У цьому прикладі:
Delivery — абстрактний базовий клас.
Trackable — інтерфейс із контрактом getTrackingInfo().
Delivery реалізує Trackable.
CourierDelivery і PickupDelivery успадковуються від Delivery.
Кожен дочірній клас реалізує абстрактні методи по-своєму.
Метод printSummary() реалізований один раз у базовому класі й доступний усім дочірнім класам.
abstract class Service {
abstract run(): void;
}
// Помилка
const service = new Service();Створюйте екземпляри конкретних дочірніх класів, які реалізували всі абстрактні члени.
superЯкщо дочірній клас має конструктор, він повинен викликати конструктор батьківського класу до використання this:
class Parent {
constructor(public readonly value: number) {}
}
class Child extends Parent {
constructor(value: number) {
super(value);
}
}interface Printable {
print(): void;
getTitle(): string;
}
class Document implements Printable {
print(): void {
console.log("Документ");
}
// Помилка: відсутній метод getTitle
}Реалізація implements повинна містити всі обов’язкові члени інтерфейсу.
extends та implementsclass Base {
save(): void {
console.log("Збереження");
}
}
interface Named {
name: string;
}
class Item extends Base implements Named {
constructor(public name: string) {
super();
}
}extends Base успадковує готовий метод save(), а implements Named лише перевіряє наявність властивості name.
Якщо метод перевизначається, його сигнатура повинна бути сумісною з батьківською:
class Parent {
getValue(): number {
return 1;
}
}
class Child extends Parent {
override getValue(): number {
return 2;
}
}Модифікатор override допомагає випадково не створити новий метод із помилковою назвою.
extends створює наслідування між класами.
Дочірній клас отримує властивості та методи батьківського класу.
super() викликає конструктор батьківського класу.
super.method() дає змогу викликати батьківську реалізацію методу.
override явно позначає перевизначення методу.
implements перевіряє, що клас відповідає інтерфейсу.
Один клас може реалізовувати кілька інтерфейсів.
Абстрактний клас не можна створити безпосередньо.
Абстрактні методи та властивості повинні бути реалізовані в неабстрактних дочірніх класах.
Абстрактний клас підходить для спільного стану й готової поведінки, а інтерфейс — для опису контракту.