Когда данные и поведение растут вместе, удобно собрать их в один тип с понятными правилами. Для этого в 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и как настроить сборку.