TypeScript Курс

Классы TypeScript и объектно-ориентированное программирование

Самоучитель Python 3, собранный из материалов данного сайта.Предназначен в основном для тех, кто хочет изучить язык программирования Python с нуля.

Введение: Строим дом для кода

Представь, что ты строишь дом. У дома есть характеристики (цвет стен, количество этажей, есть ли гараж) и умения (открывать дверь, включать свет, включать отопление). В программировании такие "дома" называются объектами. А чтобы не строить каждый дом с нуля, мы создаём чертёж — это и есть класс.

В этом уроке мы научимся создавать свои собственные "чертежи" (классы) на TypeScript. Ты узнаешь, как описывать характеристики (свойства) и умения (методы) объектов. Поехали!

Что такое класс? Аналогия с конструктором LEGO

Представь, что у тебя есть конструктор LEGO. У тебя есть инструкция, как собрать типовой домик. Эта инструкция — класс. А каждый собранный по ней домик — объект (экземпляр класса).

Класс — это шаблон, по которому создаются объекты. Он описывает:

  • Свойства (поля) — характеристики: цвет, размер, имя.
  • Методы — действия: бежать, говорить, вычислять.

В TypeScript классы помогают нам организовывать код, делать его понятным и переиспользуемым. Давай посмотрим на самый простой класс.

Создаём первый класс

Вот как выглядит класс в TypeScript:

// Объявляем класс с помощью ключевого слова class
class Cat {
  // Свойства (поля) класса
  name: string;          // Имя кота
  age: number;           // Возраст
  color: string;         // Цвет шерсти

  // Конструктор — специальный метод, который вызывается при создании нового объекта
  constructor(name: string, age: number, color: string) {
    this.name = name;    // this.name — свойство объекта, name — параметр
    this.age = age;
    this.color = color;
  }

  // Метод класса — действие, которое может выполнять объект
  meow(): void {
    console.log(`${this.name} говорит: Мяу!`);
  }

  // Ещё один метод
  introduce(): void {
    console.log(`Привет! Я ${this.name}, мне ${this.age} лет, я ${this.color} цвета.`);
  }
}

// Создаём объект (экземпляр) класса Cat
const myCat = new Cat("Барсик", 3, "рыжего");

// Вызываем методы объекта
myCat.meow();        // Выведет: Барсик говорит: Мяу!
myCat.introduce();   // Выведет: Привет! Я Барсик, мне 3 лет, я рыжего цвета.

Разберём по частям:

  • class Cat { ... } — объявление класса с именем Cat (с большой буквы — это правило).
  • name: string; — объявление свойства name типа string.
  • constructor(name: string, ...) — конструктор. Он принимает параметры и присваивает их свойствам объекта через this.
  • this.name = name — this указывает на конкретный объект (нашего Барсика).
  • new Cat("Барсик", 3, "рыжего") — создаём новый объект. Ключевое слово new вызывает конструктор.
Запомни! Класс — это чертёж. Объект — это конкретная вещь, созданная по чертежу.

Свойства и типы

В TypeScript мы обязательно указываем тип каждого свойства. Это помогает избежать ошибок.

class Student {
  // Свойства с типами
  name: string;
  age: number;
  grade: number;        // Оценка
  isActive: boolean;    // Активен ли студент

  constructor(name: string, age: number, grade: number, isActive: boolean) {
    this.name = name;
    this.age = age;
    this.grade = grade;
    this.isActive = isActive;
  }

  // Метод для вывода информации
  printInfo(): void {
    console.log(`Студент ${this.name}, возраст: ${this.age}, оценка: ${this.grade}`);
  }
}

// Создаём студента
const student1 = new Student("Аня", 14, 5, true);
student1.printInfo(); // Студент Аня, возраст: 14, оценка: 5

Если ты попытаешься присвоить свойству значение другого типа, TypeScript выдаст ошибку ещё до запуска программы. Например:

// student1.name = 123; // ОШИБКА! name должно быть строкой

Методы: действия объектов

Методы — это функции внутри класса. Они могут принимать параметры и возвращать значения.

class Calculator {
  // Метод сложения
  add(a: number, b: number): number {
    return a + b;
  }

  // Метод умножения
  multiply(a: number, b: number): number {
    return a * b;
  }

  // Метод, который использует свойства объекта
  value: number;

  constructor(value: number) {
    this.value = value;
  }

  // Увеличиваем значение на число
  increase(amount: number): void {
    this.value += amount;
  }

  // Получаем текущее значение
  getValue(): number {
    return this.value;
  }
}

const calc = new Calculator(10);
console.log(calc.add(5, 3));      // 8
console.log(calc.multiply(4, 2)); // 8
calc.increase(5);
console.log(calc.getValue());     // 15

Методы могут быть простыми (как add) или изменять состояние объекта (как increase).

Конструктор: настройка объекта при создании

Конструктор — это метод, который вызывается автоматически при создании объекта через new. Он нужен, чтобы задать начальные значения свойств.

Пример:

class Car {
  brand: string;
  model: string;
  year: number;

  // Конструктор с тремя параметрами
  constructor(brand: string, model: string, year: number) {
    this.brand = brand;
    this.model = model;
    this.year = year;
    console.log(`Создан автомобиль: ${brand} ${model} ${year} года`);
  }

  // Метод для получения полного названия
  getFullName(): string {
    return `${this.brand} ${this.model} (${this.year})`;
  }
}

const myCar = new Car("Toyota", "Camry", 2020);
console.log(myCar.getFullName()); // Toyota Camry (2020)

Конструктор может быть без параметров, если все свойства имеют значения по умолчанию.

Модификаторы доступа: public, private, protected

В TypeScript есть три уровня доступа к свойствам и методам:

  • public (по умолчанию) — доступно отовсюду.
  • private — доступно только внутри класса.
  • protected — доступно внутри класса и в его наследниках (о наследовании позже).

Зачем это нужно? Чтобы скрыть внутренние детали класса и не дать случайно их изменить.

class BankAccount {
  // Приватное свойство — доступно только внутри класса
  private balance: number;

  // Публичное свойство — доступно всем
  public owner: string;

  constructor(owner: string, initialBalance: number) {
    this.owner = owner;
    this.balance = initialBalance;
  }

  // Публичный метод для пополнения счета
  public deposit(amount: number): void {
    if (amount > 0) {
      this.balance += amount;
      console.log(`Пополнено на ${amount}. Баланс: ${this.balance}`);
    }
  }

  // Публичный метод для получения баланса (только чтение)
  public getBalance(): number {
    return this.balance;
  }

  // Приватный метод — используется только внутри класса
  private logTransaction(type: string, amount: number): void {
    console.log(`Транзакция: ${type} на сумму ${amount}`);
  }
}

const account = new BankAccount("Иван", 1000);
console.log(account.owner);       // Иван
// console.log(account.balance);  // ОШИБКА! balance — private
account.deposit(500);             // Пополнено на 500. Баланс: 1500
console.log(account.getBalance()); // 1500
// account.logTransaction("test", 100); // ОШИБКА! logTransaction — private

Используй private для данных, которые не должны меняться извне. Это защищает объект от неправильного использования.

Геттеры и сеттеры (аксессоры)

Иногда нужно контролировать, как изменяются свойства. Для этого используют геттеры (для чтения) и сеттеры (для записи). Они выглядят как свойства, но работают как методы.

class Thermometer {
  private _celsius: number;

  constructor(celsius: number) {
    this._celsius = celsius;
  }

  // Геттер для получения температуры в Цельсиях
  get celsius(): number {
    return this._celsius;
  }

  // Сеттер для установки температуры с проверкой
  set celsius(value: number) {
    if (value < -273.15) {
      console.log("Температура ниже абсолютного нуля! Устанавливаю -273.15");
      this._celsius = -273.15;
    } else {
      this._celsius = value;
    }
  }

  // Геттер для получения температуры в Фаренгейтах
  get fahrenheit(): number {
    return this._celsius * 9/5 + 32;
  }
}

const temp = new Thermometer(25);
console.log(temp.celsius);    // 25 (геттер)
console.log(temp.fahrenheit); // 77 (геттер)

temp.celsius = 30;            // сеттер
console.log(temp.celsius);    // 30

temp.celsius = -500;          // сеттер с проверкой
// Выведет: Температура ниже абсолютного нуля! Устанавливаю -273.15
console.log(temp.celsius);    // -273.15

Геттеры и сеттеры — отличный способ добавить логику при доступе к свойствам.

Статические свойства и методы

Иногда нужно, чтобы свойство или метод принадлежали не объекту, а самому классу. Такие члены называются статическими. Они вызываются через имя класса, а не через объект.

class MathHelper {
  // Статическое свойство — число Пи
  static PI: number = 3.14159;

  // Статический метод — вычисление длины окружности
  static circumference(radius: number): number {
    return 2 * MathHelper.PI * radius;
  }

  // Статический метод — площадь круга
  static area(radius: number): number {
    return MathHelper.PI * radius * radius;
  }
}

// Вызываем статические методы через имя класса
console.log(MathHelper.PI);                // 3.14159
console.log(MathHelper.circumference(5));  // 31.4159
console.log(MathHelper.area(5));           // 78.53975

Статические методы удобны для вспомогательных функций, которые не требуют создания объекта.

Наследование: классы-дети

Представь, что у тебя есть класс Animal (животное). Он имеет свойства name и age, метод makeSound(). Теперь ты хочешь создать класс Dog (собака), который тоже имеет имя и возраст, но ещё умеет fetch() (приносить палку). Вместо того чтобы писать всё с нуля, можно унаследовать класс Dog от Animal.

// Базовый класс (родитель)
class Animal {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  makeSound(): void {
    console.log(`${this.name} издаёт звук.`);
  }
}

// Дочерний класс (наследник)
class Dog extends Animal {
  // Новое свойство, которого нет в Animal
  breed: string;

  constructor(name: string, age: number, breed: string) {
    // Вызываем конструктор родителя через super()
    super(name, age);
    this.breed = breed;
  }

  // Новый метод
  fetch(): void {
    console.log(`${this.name} приносит палку!`);
  }

  // Переопределяем метод родителя
  makeSound(): void {
    console.log(`${this.name} говорит: Гав-гав!`);
  }
}

const myDog = new Dog("Рекс", 5, "Лабрадор");
myDog.makeSound();   // Рекс говорит: Гав-гав! (переопределённый метод)
myDog.fetch();       // Рекс приносит палку! (новый метод)
console.log(myDog.name); // Рекс (унаследованное свойство)

Ключевые моменты:

  • Используем extends для наследования.
  • В конструкторе дочернего класса нужно вызвать super() — конструктор родителя.
  • Можно добавлять новые свойства и методы.
  • Можно переопределять (заменять) методы родителя.

Абстрактные классы

Иногда нужно создать класс, который нельзя напрямую использовать (создавать объекты), но который задаёт общую структуру для других классов. Это абстрактный класс. Он может содержать абстрактные методы (без реализации), которые должны быть реализованы в наследниках.

// Абстрактный класс — не может быть создан напрямую
abstract class Shape {
  abstract getArea(): number; // Абстрактный метод — без тела

  // Обычный метод
  describe(): void {
    console.log(`Это фигура с площадью ${this.getArea()}`);
  }
}

class Circle extends Shape {
  radius: number;

  constructor(radius: number) {
    super();
    this.radius = radius;
  }

  // Обязательно реализуем абстрактный метод
  getArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

class Square extends Shape {
  side: number;

  constructor(side: number) {
    super();
    this.side = side;
  }

  getArea(): number {
    return this.side * this.side;
  }
}

// const shape = new Shape(); // ОШИБКА! Нельзя создать объект абстрактного класса

const myCircle = new Circle(5);
const mySquare = new Square(4);

console.log(myCircle.getArea()); // 78.53981633974483
console.log(mySquare.getArea()); // 16
myCircle.describe(); // Это фигура с площадью 78.53981633974483

Абстрактные классы гарантируют, что все наследники будут иметь определённые методы.

Интерфейсы и классы

Интерфейс — это описание структуры объекта: какие свойства и методы он должен иметь. Класс может реализовать интерфейс, то есть обязаться выполнить его требования.

// Интерфейс для любого, кто может летать
interface Flyable {
  fly(): void;
  land(): void;
}

// Класс, реализующий интерфейс Flyable
class Airplane implements Flyable {
  fly(): void {
    console.log("Самолёт летит.");
  }

  land(): void {
    console.log("Самолёт приземляется.");
  }
}

// Другой класс, тоже реализующий Flyable
class Bird implements Flyable {
  fly(): void {
    console.log("Птица машет крыльями и летит.");
  }

  land(): void {
    console.log("Птица садится на ветку.");
  }
}

const boeing = new Airplane();
const sparrow = new Bird();

boeing.fly();   // Самолёт летит.
sparrow.fly();  // Птица машет крыльями и летит.

Интерфейсы помогают писать гибкий код: ты можешь работать с любым объектом, который реализует нужный интерфейс, не заботясь о его конкретном типе.

Практические задания

Теперь твоя очередь! Попробуй выполнить эти задания. Если что-то не получается — перечитай урок или попроси помощи.

Задание 1: Класс Book (Книга)

Создай класс Book со следующими свойствами:

  • title (string) — название книги
  • author (string) — автор
  • year (number) — год издания
  • isAvailable (boolean) — доступна ли книга (по умолчанию true)

Методы:

  • borrow() — если книга доступна, делает её недоступной и выводит сообщение "Книга взята". Если нет — выводит "Книга уже на руках".
  • returnBook() — делает книгу доступной и выводит "Книга возвращена".
  • getInfo() — возвращает строку с информацией о книге (название, автор, год).

Подсказка: используй private для isAvailable и геттер/сеттер для доступа.

Задание 2: Класс Library (Библиотека)

Создай класс Library, который управляет книгами.

  • Свойство books — массив объектов класса Book (из задания 1).
  • Метод addBook(book: Book) — добавляет книгу в библиотеку.
  • Метод findBook(title: string) — ищет книгу по названию и возвращает её (или undefined).
  • Метод borrowBook(title: string) — находит книгу и вызывает её метод borrow().
  • Метод returnBook(title: string) — находит книгу и вызывает её метод returnBook().

Подсказка: используй массив и методы поиска.

Задание 3: Класс User и наследование

Создай базовый класс User со свойствами:

  • username (string)
  • email (string)

Метод displayInfo() выводит "Пользователь: {username}, email: {email}".

Создай два дочерних класса:

  • Admin — добавляет свойство role (string) со значением "admin". Переопределяет displayInfo(), чтобы выводить "Администратор: {username}, email: {email}". Добавляет метод deleteUser(user: User), который выводит "Пользователь {user.username} удалён".
  • Moderator — добавляет свойство sections (массив строк) — разделы, которые он модерирует. Переопределяет displayInfo(), чтобы выводить "Модератор: {username}, email: {email}". Добавляет метод banUser(user: User), который выводит "Пользователь {user.username} забанен".

Подсказка: используй super() в конструкторе и переопределение методов.

Совет: После написания кода, попробуй создать несколько объектов и вызвать их методы. Убедись, что всё работает правильно. Если возникли ошибки — внимательно прочитай сообщение об ошибке, оно часто подсказывает, что не так.

Поздравляю! Ты изучил основы классов в TypeScript. Теперь ты можешь создавать свои собственные типы данных с поведением. Это мощный инструмент, который поможет тебе писать чистый, организованный и переиспользуемый код.