Тут насправді все просто. Робота з класами в TS практично не відрізняється від класичного JS, але TS його розширює, додаючи нові механіки, які можуть зробити вашу роботу легше.
Класи TS
В прикладі ви можете побачити як зазначити тими властивостей, змінних та методів в TS.
class User {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): void {
console.log(`Привіт, мене звати ${this.name} і мені ${this.age} років.`);
}
}
const user = new User("Олексій", 25);
user.greet(); // Привіт, мене звати Олексій і мені 25 років.
Але тут є деякі користні штуки. TypeScript розширює базову роботу з класами, додаючи до них типи змінних, що дійсно користно:
public
- Доступний всюди (за замовчуванням).private
- Доступний тільки всередині цього класу.protected
- Доступний всередині класу і його нащадків.
Використовуючи ці модифікатори доступу, ви вожете легко керувати доступом до ваших данних:
class BankAccount {
private balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
deposit(amount: number) {
this.balance += amount;
console.log(`Додано ${amount}. Баланс: ${this.balance}`);
}
getBalance() {
return this.balance;
}
}
const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
// console.log(account.balance); // ❌ Помилка: balance є private
Дорочі в ECMA2020 додалолися приватны класи які можна використовувати без TS:
class User {
#password; // Приватне поле (доступне тільки всередині класу)
constructor(name, password) {
this.name = name;
this.#password = password;
}
checkPassword(input) {
return this.#password === input;
}
}
const user = new User("Олексій", "secret");
console.log(user.name); // ✅ Олексій
console.log(user.#password); // ❌ Помилка! Приватне поле
console.log(user.checkPassword("secret")); // ✅ true
Але це уже оффтоп, тому їдемо далі.
Наслідування Extends
Наслідування дозволяє створювати класи-нащадки, які розширюють функціональність батьківського класу. Наприклад:
class Animal {
constructor(public name: string) {}
makeSound(): void {
console.log("Звук тварини");
}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name);
}
makeSound(): void {
console.log("Гав-гав!");
}
}
const dog = new Dog("Барсик", "Лабрадор");
dog.makeSound(); // Гав-гав!
- Ключове слово super() викликає constructor батьківського класу.
- Метод makeSound() перевизначається у Dog.
Такі розширення можна зробити і в нативному JS, але в TS це є з коробки, тому думаю робота з класами, це щось перше користне, що TS приносить в JS, але далі більше.
Абстрактні класи
Абстрактний клас у TypeScript — це клас, який не можна створити напряму, але можна наслідувати. Він використовується, коли треба задати базову логіку, яку потім реалізують нащадки.
Оголошення абстрактного класу:
abstract class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
// Абстрактний метод (без реалізації)
abstract makeSound(): void;
// Звичайний метод (має реалізацію)
move(): void {
console.log(`${this.name} рухається.`);
}
}
// ❌ Помилка! Не можна створити екземпляр абстрактного класу.
// const animal = new Animal("Звір");
class Dog extends Animal {
makeSound(): void {
console.log("Гав-гав!");
}
}
const dog = new Dog("Барсик");
dog.makeSound(); // ✅ Гав-гав!
dog.move(); // ✅ Барсик рухається.
Абстрактний клас не можна створити або запросити напряму, це викличе помилку. Методи abstract, які ви зарееструали, потрібно реалізувати в дочірньому класі (Dog), який наслідує абстракт. Також абстрактний клас може містити як абстрактні (makeSound), так і звичайні методи (move), які попадуть з кінцеву збірку JS.
Абстрактні класи ще можуть містити абстрактні властивості, які нащадки повинні реалізувати обовʼязково!
abstract class Employee {
abstract role: string; // Абстрактне поле
showRole(): void {
console.log(`Посада: ${this.role}`);
}
}
class Developer extends Employee {
role = "Frontend Developer"; // ✅ Обов'язково реалізувати
}
const dev = new Developer();
dev.showRole(); // ✅ Посада: Frontend Developer
Якщо role не буде оголошено у Developer, TypeScript видасть помилку. Причому наслідування та розширення - можна створювати кілька рівнів успадкування:
abstract class Transport {
abstract move(): void;
}
class Car extends Transport {
move(): void {
console.log("Автомобіль їде по дорозі.");
}
}
class Plane extends Transport {
move(): void {
console.log("Літак летить у небі.");
}
}
const car = new Car();
car.move(); // ✅ Автомобіль їде по дорозі.
const plane = new Plane();
plane.move(); // ✅ Літак летить у небі.
Всі нащадки Transport повинні реалізовувати метод move(), бо він абстрактний. Ще абстрактний клас може реалізовувати інтерфейс, про які правда ми поговоримо трохи нижче.
interface Loggable {
log(): void;
}
abstract class Logger implements Loggable {
abstract log(): void; // Реалізація інтерфейсу
}
class FileLogger extends Logger {
log(): void {
console.log("Лог записано у файл.");
}
}
const logger = new FileLogger();
logger.log(); // ✅ Лог записано у файл.
І щоб було повне розуміння, як використовують абтрактні класи на прикладі реальних задач:
abstract class Database {
abstract connect(): void;
abstract disconnect(): void;
}
class MySQLDatabase extends Database {
connect(): void {
console.log("Підключення до MySQL...");
}
disconnect(): void {
console.log("Відключення від MySQL...");
}
}
const db = new MySQLDatabase();
db.connect(); // ✅ Підключення до MySQL...
db.disconnect(); // ✅ Відключення від MySQL...
Інтерфейси в класах чи implements
Памʼятаєте ми з вами обговорювати інтерфейси, які стосувалися обʼєктів. Так ось implements це практично теж саме, але для класів. Інтерфейси класів фактично визначають структуру, якій має відповідати клас. Вони не обмежують додавання нових методів, але вказують на мінімальну їх кількість.
interface Logger {
log(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`Лог: ${message}`);
}
}
const logger = new ConsoleLogger();
logger.log("Це тестовий лог."); // Лог: Це тестовий лог.
Це корректний інтерфейс класу, але якщо в інтерфейс додати метод, якого не буде в класі, тоді ви отримаєте помилку:
interface Logger {
log(message: string): void;
gvozd(message: string): void;
}
class ConsoleLogger implements Logger { // Помилка gvozd не знайдений в ConsoleLogger
log(message: string): void {
console.log(`Лог: ${message}`);
}
vak(text: string): string { // Доданий метод не викликає ніяких помилок
return text
}
}
const logger = new ConsoleLogger();
logger.vak("Це тестовий лог.");
Якщо ж вам потрібно, щоб неможна було додавати в класс незареестрованих методів, тоді ви можете використовувати абстрактні класи, про які ми говорили вище.
Декоратори (@decorators)
Нажаль покищо, станом на 2025 рік, вони на 100% не підтримуються із коробки. По крайній мірі весь заявенний в них функціонал, тому ми обговоримо тільки те, що зараз доступно, але для початку, вам потрібно їх ввімкнути опцією:
{
"compilerOptions": {
"experimentalDecorators": true
}
}
Декторатори це спеціальні функції, які будуть змінювати поведінку класів, або методів, тим самим корегуючи їх функціонал. Це користно, коли у вас є багато методів, але до деяких треба внести корректировки.
function Logger(constructor: Function) {
console.log("Лог класу:", constructor.name);
}
@Logger
class Person {
constructor(public name: string) {}
}
const person = new Person("Олексій");
// Лог класу: Person
В прикладі ми бачимо декоратор Logger, який ми застосували до класу Person і в якому напочатку не було constructor. Але декоратор його додав. Я це використовую як розширення функцій та методів, і інколи це може бути дуже користно.
Щоб було зрозуміло наскільки це мощна штука, я покажу приклад реального коду:
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value; // Зберігаємо оригінальний метод
descriptor.value = function (...args: any[]) {
console.log(`Виклик методу: ${propertyKey} з аргументами: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args); // Викликаємо оригінальний метод
console.log(`Результат методу ${propertyKey}: ${JSON.stringify(result)}`);
return result;
};
return descriptor;
}
class Calculator {
@LogMethod
add(a: number, b: number): number {
return a + b;
}
@LogMethod
subtract(a: number, b: number): number {
return a - b;
}
@LogMethod
multiply(a: number, b: number): number {
return a * b;
}
}
const calc = new Calculator();
console.log(calc.add(5, 3));
console.log(calc.subtract(10, 4));
console.log(calc.multiply(6, 7));
Також треба враховати, що декоратори це застосуючі функції, які рееструються в JS, а це значить, що ми можем використовувати в них параметри:
function LogMethodConditional(enabled: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
if (!enabled) return descriptor; // Якщо логування вимкнене, повертаємо оригінальний метод без змін
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Виклик методу: ${propertyKey} з аргументами: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`Результат методу ${propertyKey}: ${JSON.stringify(result)}`);
return result;
};
return descriptor;
};
}
class MathOperations {
@LogMethodConditional(true) // Логування увімкнено
divide(a: number, b: number): number {
return a / b;
}
@LogMethodConditional(false) // Логування вимкнено
modulo(a: number, b: number): number {
return a % b;
}
}
const math = new MathOperations();
console.log(math.divide(10, 2)); // ✅ Логування буде
console.log(math.modulo(10, 3)); // ❌ Логування не буде
Геттери (get) та сеттери (set)
І на останок гетери та сетери, це сами просто що може бути. По факту з звичайним JS вони не змінюються, додається лише тип:
class User {
private _age: number = 0;
get age(): number {
return this._age;
}
set age(value: number) {
if (value < 0) {
console.log("Вік не може бути від’ємним!");
} else {
this._age = value;
}
}
}
const user = new User();
user.age = 25; // ✅ Все добре
console.log(user.age); // 25
user.age = -5; // ❌ Вік не може бути від’ємним!
Але якщо додати до них декоратори, добре нагнітати не буду )))