📘 TypeScript

OOP: Classes and Objects in TypeScript: A Detailed Guide

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты — архитектор, который проектирует дома. Ты не можешь каждый раз изобретать колесо: сначала придумывать, где будут стены, потом — как открываются двери, потом — какой формы окна. Вместо этого ты создаёшь чертёж дома. На чертеже указано: у дома будет 4 стены, 2 двери, 5 окон, крыша. Но сам чертёж — это ещё не дом. Это просто описание.

Когда приходит заказчик, ты берёшь этот чертёж и строишь по нему конкретный дом. Потом приходит второй заказчик — ты строишь второй дом по тому же чертежу. Дома могут отличаться цветом стен или материалом крыши, но структура у них одинаковая.

В программировании работает та же логика:

  • Класс — это чертёж. Он описывает, какие свойства (цвет, размер, имя) и методы (действия: идти, говорить, считать) будут у объекта.
  • Объект — это конкретный дом, построенный по чертежу. У каждого объекта свои значения свойств, но структура — из класса.
💡 Важно: Класс — это тип данных, который ты сам придумываешь. В TypeScript классы помогают организовать код так, чтобы он был похож на реальный мир: у тебя есть «сущности» (пользователи, машины, заказы), и каждая сущность знает, что она умеет делать.

Зачем это нужно? Представь, что ты пишешь игру. У тебя 100 врагов. Без классов тебе пришлось бы 100 раз писать одно и то же: «у этого врага 10 жизней, он умеет атаковать, он умеет получать урон». С классами ты пишешь один раз чертёж «Враг», а потом создаёшь 100 объектов-врагов. Если нужно добавить всем врагам новое умение — ты меняешь только класс, и все 100 врагов получают это умение автоматически.

Теория: как это работает изнутри

Когда ты объявляешь класс, TypeScript (а под капотом — JavaScript) создаёт функцию-конструктор. Эта функция умеет создавать новые объекты и наделять их свойствами и методами.

Внутри класса есть три ключевых элемента:

  1. Поля (свойства) — переменные, которые принадлежат объекту. Например, name: string или age: number.
  2. Конструктор — специальный метод constructor(), который вызывается автоматически, когда ты создаёшь новый объект. В нём ты задаёшь начальные значения свойств.
  3. Методы — функции, которые принадлежат объекту. Они могут менять свойства объекта или выполнять какие-то действия.

Когда ты пишешь new ClassName(), происходит следующее:

  • Создаётся пустой объект.
  • К этому объекту привязываются все методы класса (через прототип).
  • Вызывается конструктор, который заполняет свойства объекта.
  • Объект возвращается и сохраняется в переменную.
⚠️ Важно: В TypeScript есть модификаторы доступа: public (доступно всем), private (доступно только внутри класса), protected (доступно внутри класса и его наследников). Это защищает данные от случайного изменения.

Синтаксис в TypeScript: все формы и варианты

Давай разберём синтаксис класса от простого к сложному. Вот базовая структура:

// Объявляем класс с именем "Person"
class Person {
  // Свойства (поля) с указанием типов
  name: string;
  age: number;

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

  // Метод — функция внутри класса
  greet(): void {
    console.log(`Привет, я ${this.name}!`);
  }
}

// Создаём объект (экземпляр класса)
const person1 = new Person("Анна", 25);
person1.greet(); // Выведет: "Привет, я Анна!"

Варианты синтаксиса:

Конструкция Описание Пример
Свойства с модификаторами Указываем доступ: public, private, protected private age: number
Сокращённая запись свойств Свойства объявляются прямо в конструкторе constructor(public name: string)
Статические свойства/методы Принадлежат классу, а не объекту static count: number = 0
Геттеры и сеттеры Контролируемый доступ к свойствам get fullName()
Абстрактные классы Нельзя создать объект, только наследовать abstract class Animal

Сокращённая запись свойств в конструкторе:

class User {
  // Свойства объявлены прямо в параметрах конструктора
  constructor(
    public name: string,
    private age: number,
    protected email: string
  ) {}

  public getAge(): number {
    return this.age; // Доступно, потому что мы внутри класса
  }
}

const user = new User("Иван", 30, "ivan@mail.ru");
console.log(user.name); // ✅ Доступно (public)
// console.log(user.age);  // ❌ Ошибка: age — private
// console.log(user.email); // ❌ Ошибка: email — protected

Примеры кода — от простого к сложному

Пример 1: Простейший класс "Книга"

// Класс описывает книгу
class Book {
  title: string;      // Название книги
  author: string;     // Автор
  pages: number;      // Количество страниц

  // Конструктор: задаём начальные значения
  constructor(title: string, author: string, pages: number) {
    this.title = title;
    this.author = author;
    this.pages = pages;
  }

  // Метод: выводит информацию о книге
  getSummary(): string {
    return `"${this.title}" — ${this.author}, ${this.pages} стр.`;
  }
}

// Создаём объект книги
const myBook = new Book("Война и мир", "Лев Толстой", 1225);
console.log(myBook.getSummary()); // "Война и мир" — Лев Толстой, 1225 стр.

Пример 2: Класс с приватными полями и геттерами

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

  constructor(initialBalance: number) {
    this._balance = initialBalance;
  }

  // Геттер — позволяет читать приватное поле
  get balance(): number {
    return this._balance;
  }

  // Сеттер — позволяет безопасно менять приватное поле
  set balance(amount: number) {
    if (amount < 0) {
      console.log("Баланс не может быть отрицательным!");
      return;
    }
    this._balance = amount;
  }

  // Метод для пополнения счёта
  deposit(amount: number): void {
    if (amount > 0) {
      this._balance += amount;
      console.log(`Пополнено на ${amount}. Баланс: ${this._balance}`);
    }
  }
}

const account = new BankAccount(1000);
console.log(account.balance); // 1000 (через геттер)
account.deposit(500);         // Пополнено на 500. Баланс: 1500
account.balance = 2000;       // Используем сеттер
console.log(account.balance); // 2000

Пример 3: Статические члены класса

class MathHelper {
  // Статическое свойство — принадлежит классу, а не объекту
  static pi: number = 3.14159;

  // Статический метод — вызывается без создания объекта
  static circleArea(radius: number): number {
    return MathHelper.pi * radius * radius;
  }
}

// Вызываем статический метод напрямую через класс
const area = MathHelper.circleArea(5);
console.log(`Площадь круга: ${area}`); // Площадь круга: 78.53975

// Создавать объект не нужно!
// const helper = new MathHelper(); // ❌ Не имеет смысла

Пример 4: Наследование классов

// Базовый класс "Животное"
class Animal {
  constructor(public name: string) {}

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

// Дочерний класс "Собака" наследует Animal
class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name); // Вызываем конструктор родителя
  }

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

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

const dog = new Dog("Бобик", "Овчарка");
dog.makeSound(); // Бобик лает: Гав-гав!
dog.fetch();     // Бобик приносит палку

Пример 5: Абстрактный класс

// Абстрактный класс — нельзя создать объект, только наследовать
abstract class Shape {
  constructor(public color: string) {}

  // Абстрактный метод — должен быть реализован в наследнике
  abstract getArea(): number;

  // Обычный метод — доступен всем наследникам
  getColor(): string {
    return this.color;
  }
}

class Circle extends Shape {
  constructor(color: string, public radius: number) {
    super(color);
  }

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

const circle = new Circle("красный", 10);
console.log(circle.getArea()); // 314.159...
// const shape = new Shape("синий"); // ❌ Ошибка: нельзя создать объект абстрактного класса

Пример 6: Реализация интерфейса

// Интерфейс — описывает структуру, но не содержит реализации
interface Flyable {
  fly(): void;
  land(): void;
}

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

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

class Bird implements Flyable {
  fly(): void {
    console.log("Птица машет крыльями");
  }

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

// Полиморфизм: работаем с разными классами через общий интерфейс
function startFlight(flyable: Flyable): void {
  flyable.fly();
}

startFlight(new Airplane()); // Самолёт летит
startFlight(new Bird());     // Птица машет крыльями

Пример 7: Композиция классов

class Engine {
  start(): void {
    console.log("Двигатель запущен");
  }
}

class Wheels {
  rotate(): void {
    console.log("Колёса крутятся");
  }
}

// Класс Car использует другие классы (композиция)
class Car {
  private engine: Engine;
  private wheels: Wheels;

  constructor() {
    this.engine = new Engine();
    this.wheels = new Wheels();
  }

  drive(): void {
    this.engine.start();
    this.wheels.rotate();
    console.log("Машина поехала!");
  }
}

const myCar = new Car();
myCar.drive();
// Двигатель запущен
// Колёса крутятся
// Машина поехала!

Сравнение с Python: чем похоже и чем отличается

Характеристика TypeScript Python
Объявление класса class Name {} class Name:
Конструктор constructor() def __init__(self)
Ссылка на текущий объект this self
Типизация свойств Обязательна: name: string Необязательна (динамическая)
Приватные поля private (на уровне языка) __name (соглашение, не строгое)
Геттеры/сеттеры get/set @property
Статические методы static method() @staticmethod
Абстрактные классы abstract class ABC из модуля abc
Интерфейсы Есть (interface) Нет (используются абстрактные классы)
💡 Главное отличие: TypeScript — статически типизированный язык. Ты обязан указать типы свойств и параметров. Python — динамический, типы проверяются во время выполнения. TypeScript компилируется в JavaScript, Python — интерпретируемый.

Типичные ошибки и подводные камни

  1. Забыть вызвать super() в наследнике
    class Parent {
      constructor(public name: string) {}
    }
    
    class Child extends Parent {
      constructor(name: string, public age: number) {
        // ❌ Забыли super(name)
      }
    }

    TypeScript выдаст ошибку: Constructors for derived classes must contain a 'super' call.

  2. Путаница между this и super

    this — ссылается на текущий объект. super — ссылается на родительский класс. Нельзя использовать this до вызова super() в конструкторе наследника.

  3. Изменение приватного поля напрямую
    class User {
      private password: string = "123";
    }
    
    const user = new User();
    // user.password = "456"; // ❌ Ошибка: password — private

    Используй геттеры/сеттеры или публичные методы для доступа.

  4. Забыть указать тип свойства
    class Item {
      name; // ❌ Не указан тип — будет any
    }

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

  5. Создание объекта без new
    const obj = MyClass(); // ❌ Ошибка: нельзя вызвать класс как функцию

    Всегда используй new.

  6. Переопределение метода с неправильной сигнатурой
    class Base {
      greet(name: string): void {}
    }
    
    class Derived extends Base {
      // ❌ Параметр другого типа
      greet(name: number): void {}
    }

    TypeScript не разрешит изменить типы параметров при переопределении.

  7. Использование protected там, где нужно private

    protected — доступно наследникам. Если наследникам не нужно видеть поле, используй private.

Лучшие практики и советы профессионалов

  1. Принцип единственной ответственности. Один класс — одна задача. Не делай класс «Бог», который умеет всё. Разделяй на маленькие классы.
  2. Используй сокращённую запись свойств в конструкторе. Это уменьшает количество кода и делает его чище.
  3. Делай поля приватными по умолчанию. Открывай доступ только через методы или геттеры. Это защищает данные.
  4. Предпочитай композицию наследованию. Вместо «Автомобиль наследует Двигатель» лучше «Автомобиль содержит Двигатель».
  5. Используй интерфейсы для описания контрактов. Это делает код гибким и тестируемым.
  6. Не злоупотребляй статическими методами. Они удобны для утилит, но усложняют тестирование, если используются для хранения состояния.
  7. Всегда указывай возвращаемый тип методов. Даже если это void. Это улучшает читаемость.
  8. Используй readonly для неизменяемых полей. Если поле не должно меняться после создания объекта, пометь его readonly.
🎯 Совет: Думай о классах как о «чертежах реальных сущностей». Если ты описываешь «Студента», у него должны быть свойства (имя, возраст, курс) и методы (учиться, сдавать экзамен). Не добавляй в класс «Студент» методы для управления базой данных — это задача другого класса.

Вопросы для самопроверки

  1. Вопрос: В чём разница между классом и объектом?

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

  2. Вопрос: Что такое конструктор и зачем он нужен?

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

  3. Вопрос: Какие модификаторы доступа есть в TypeScript?

    Ответ: public (доступно всем), private (только внутри класса), protected (внутри класса и его наследников).

  4. Вопрос: Чем отличается статический метод от обычного?

    Ответ: Статический метод принадлежит классу, а не объекту. Его вызывают через имя класса (например, MathHelper.circleArea()), без создания экземпляра.

  5. Вопрос: Что такое наследование и зачем оно нужно?

    Ответ: Наследование позволяет создать новый класс на основе существующего. Дочерний класс получает все свойства и методы родителя и может добавлять свои или переопределять существующие. Это уменьшает дублирование кода.

  6. Вопрос: Что произойдёт, если в дочернем классе не вызвать super()?

    Ответ: TypeScript выдаст ошибку компиляции. Конструктор дочернего класса обязан вызвать конструктор родителя через super().

  7. Вопрос: Чем абстрактный класс отличается от обычного?

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

  8. Вопрос: Что такое интерфейс и как он связан с классом?

    Ответ: Интерфейс описывает структуру (какие свойства и методы должны быть), но не содержит реализации. Класс может реализовать интерфейс через ключевое слово implements, и тогда он обязан предоставить реализацию всех методов интерфейса.

  9. Вопрос: В чём разница между public readonly name: string и private name: string?

    Ответ: readonly означает, что свойство можно прочитать, но нельзя изменить после инициализации. private означает, что свойство нельзя прочитать или изменить снаружи класса. Можно комбинировать: private readonly.

  10. Вопрос: Почему не стоит делать все поля публичными?

    Ответ: Публичные поля нарушают инкапсуляцию — любой код может изменить их напрямую, что может привести к некорректному состоянию объекта. Лучше контролировать доступ через методы и геттеры/сеттеры.

Итог и что изучить дальше

Мы разобрали фундамент объектно-ориентированного программирования в TypeScript. Ты узнал:

  • Что класс — это чертёж, а объект — конкретная реализация.
  • Как объявлять классы, свойства, методы, конструкторы.
  • Как работают модификаторы доступа, наследование, абстрактные классы и интерфейсы.
  • Как избегать типичных ошибок и писать чистый код.

Теперь, когда ты освоил базу, вот что стоит изучить дальше:

  1. Дженерики (Generics) — создание классов и функций, которые работают с разными типами данных.
  2. Декораторы — мощный инструмент для изменения поведения классов (используется в Angular).
  3. SOLID — пять принципов объектно-ориентированного проектирования, которые помогут писать поддерживаемый код.
  4. Паттерны проектирования — готовые решения типовых задач (Одиночка, Фабрика, Стратегия и другие).
  5. TypeScript с фреймворками — попробуй применить знания в React, Angular или NestJS.
🚀 Напутствие: Не пытайся выучить всё сразу. Напиши несколько своих классов: «Студент», «Автомобиль», «Заказ в интернет-магазине». Поэкспериментируй с наследованием и интерфейсами. Лучший способ понять ООП — писать код каждый день.