← назад к разделу

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

Зачем нужны классы

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

В JavaScript классы тоже есть, но TypeScript добавляет к ним типы и модификаторы доступа — то есть проверки, которые ловят ошибки ещё до запуска кода.

class User {
  name: string; // поле и его тип
  age: number;

  constructor(name: string, age: number) {
    this.name = name; // конструктор заполняет поля
    this.age = age;
  }

  greet(): string {
    return `Привет, я ${this.name}`; // метод
  }
}

const u = new User("Аня", 30); // создаём объект (экземпляр)
console.log(u.greet()); // Привет, я Аня

Короткая формула: класс описывает форму, new создаёт конкретный объект по этой форме.

Поля и конструктор

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

TypeScript умеет сокращать рутину. Если добавить модификатор доступа прямо в параметр конструктора, поле объявится и заполнится автоматически:

class User {
  // public name + public age создаются и присваиваются автоматически
  constructor(public name: string, public age: number) {}
}

const u = new User("Аня", 30);
console.log(u.name); // Аня

Поле можно пометить как readonly — тогда его разрешено задать только в конструкторе, а потом менять нельзя:

class Order {
  constructor(readonly id: string) {}
}

const o = new Order("A-1");
// o.id = "A-2"; // ошибка компиляции: id только для чтения

Модификаторы доступа

Модификаторы решают, кто имеет право обращаться к полю или методу. Их три:

  • public — доступно везде (это значение по умолчанию).
  • private — доступно только внутри самого класса.
  • protected — доступно внутри класса и его наследников.
class Account {
  private balance = 0; // снаружи не виден

  deposit(amount: number): void {
    this.balance += amount; // внутри класса — пожалуйста
  }

  getBalance(): number {
    return this.balance;
  }
}

const a = new Account();
a.deposit(100);
// a.balance; // ошибка: balance приватный
console.log(a.getBalance()); // 100

Важно понимать: private и protected — это проверки на этапе компиляции. Они защищают от случайных ошибок в коде, но в готовом JavaScript поле всё ещё доступно. Если нужна настоящая приватность во время выполнения, используют синтаксис JavaScript с #:

class Account {
  #balance = 0; // приватность на уровне исполнения

  deposit(amount: number): void {
    this.#balance += amount;
  }
}

Наследование

Наследование позволяет создать класс на основе другого, переиспользовав его поля и методы. Дочерний класс объявляется через extends, а к родителю обращаются через super.

class Animal {
  constructor(protected name: string) {}

  describe(): string {
    return `Это ${this.name}`;
  }
}

class Dog extends Animal {
  constructor(name: string, private breed: string) {
    super(name); // вызов конструктора родителя — обязателен
  }

  describe(): string {
    // переопределяем метод, но переиспользуем родительский
    return `${super.describe()}, порода ${this.breed}`;
  }
}

const d = new Dog("Рекс", "овчарка");
console.log(d.describe()); // Это Рекс, порода овчарка

Когда дочерний класс задаёт свою версию метода с тем же именем, это называется переопределением (override). С TypeScript 5 такой метод можно явно пометить ключевым словом override — компилятор проверит, что в родителе действительно есть метод с таким именем.

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

Абстрактный класс — это заготовка, от которой нельзя создать объект напрямую. Он задаёт общий каркас и оставляет часть методов незаполненными (abstract), требуя, чтобы наследники их реализовали.

abstract class Shape {
  abstract area(): number; // без тела — обязателен у наследника

  describe(): string {
    return `Площадь: ${this.area()}`; // общий метод уже готов
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

// new Shape(); // ошибка: нельзя создать абстрактный класс
const c = new Circle(2);
console.log(c.describe()); // Площадь: 12.566...

Абстрактный класс полезен, когда у группы классов есть общая логика, но каждый по-своему реализует одну-две детали.

Классы и интерфейсы

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

interface Repository {
  save(value: string): void;
  findAll(): string[];
}

class InMemoryRepository implements Repository {
  private items: string[] = [];

  save(value: string): void {
    this.items.push(value);
  }

  findAll(): string[] {
    return this.items;
  }
}

Если класс забудет какой-то метод из интерфейса, компилятор сразу укажет на это. В чём разница с абстрактным классом: интерфейс — это только описание формы (никакого кода и состояния), а абстрактный класс может нести и готовую логику, и поля. Класс наследует один класс, но может реализовать несколько интерфейсов.

Декораторы: что это и зачем

Декоратор — это функция, которую «навешивают» на класс, метод, поле или параметр через синтаксис @ИмяДекоратора. Она получает то, к чему прикреплена, и может добавить поведение или метаданные — не меняя сам исходный код вручную.

Самостоятельно писать декораторы новичку почти не приходится. Но сталкиваются с ними сразу, как только берут фреймворк. Эталонный пример — NestJS, популярный backend-фреймворк на Node, где декораторы повсюду:

@Controller("users") // класс становится контроллером для пути /users
class UsersController {
  @Get(":id") // метод обрабатывает GET /users/:id
  findOne(@Param("id") id: string): string {
    return `Пользователь ${id}`;
  }
}

Здесь @Controller, @Get и @Param — декораторы. Они не выполняют запрос сами по себе, а помечают класс и метод метаданными: «это контроллер», «этот метод отвечает на GET», «возьми параметр id из адреса». Фреймворк читает эти пометки и собирает из них маршрутизацию. То есть декоратор — это способ описать намерение коротко и декларативно, отдав рутину фреймворку.

Как включить декораторы

Декораторы в экосистеме NestJS используют более раннюю их версию, поэтому их нужно явно включить в tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
  • experimentalDecorators — разрешает сам синтаксис @.
  • emitDecoratorMetadata — добавляет информацию о типах в скомпилированный код; NestJS использует её, чтобы понимать, какие зависимости подставить.

Стоит знать, что в TypeScript 5 появились стандартные декораторы (по новой спецификации языка) — они работают без флага experimentalDecorators. Но у них другой формат, и фреймворки вроде NestJS пока опираются на «экспериментальную» версию. Поэтому, когда подключаете готовый фреймворк, просто ставьте флаги, которые требует его документация.

Коротко

  • Класс собирает данные и поведение в один тип; new создаёт по нему объект.
  • Конструктор готовит объект; модификатор в параметре сразу объявляет и заполняет поле, readonly запрещает менять его после создания.
  • Модификаторы доступа public/private/protected — проверки на этапе компиляции; для настоящей приватности есть синтаксис #.
  • extends даёт наследование (через super зовём родителя), abstract задаёт каркас без возможности создать объект.
  • interface + implements описывают контракт без кода; класс реализует несколько интерфейсов, но наследует один класс.
  • Декоратор — функция-пометка через @; на них держится NestJS (@Controller, @Get).
  • Для NestJS-декораторов включите experimentalDecorators и emitDecoratorMetadata в tsconfig.json.

Что почитать дальше

  • Базовые типы TypeScript — фундамент, на котором стоят поля классов и интерфейсы.
  • Дженерики — как сделать классы и методы переиспользуемыми для разных типов.
  • Инструментарий и tsconfig — где живут флаги вроде experimentalDecorators и как настроить сборку.