TypeScript в NestJS

10 сентября 2026
Автор

Антон Ларичев

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

Почему NestJS требует TypeScript

NestJS использует механизм reflect-metadata для реализации dependency injection. Во время компиляции TypeScript сохраняет типы аргументов конструктора в метаданных класса. NestJS читает эти метаданные в runtime и автоматически разрешает зависимости.

Без TypeScript этот механизм не работает — именно поэтому NestJS требует TypeScript, а не просто рекомендует его.

Для включения записи метаданных в tsconfig.json должны быть активированы два флага:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "ES2021",
    "module": "commonjs",
    "strict": true,
    "strictNullChecks": true
  }
}

emitDecoratorMetadata — ключевой флаг: без него NestJS не сможет определить типы зависимостей и контейнер IoC перестанет работать.

Курс по теме

Освоить TypeScript с нуля на курсе

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Декораторы как основа NestJS

Все ключевые абстракции NestJS выражены через декораторы. Рассмотрим их по слоям.

Декоратор @Module

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

@Module принимает объект типа ModuleMetadata. Это обычный TypeScript-интерфейс, и IDE подсветит ошибку, если передать неверное поле.

Декоратор @Injectable

import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
  private readonly users: User[] = [];

  findAll(): User[] {
    return this.users;
  }

  findOne(id: number): User | undefined {
    return this.users.find(u => u.id === id);
  }
}

@Injectable() помечает класс как провайдер, которым может управлять контейнер NestJS. Декоратор также является триггером для emitDecoratorMetadata — TypeScript записывает типы параметров конструктора в метаданные именно в момент применения декоратора к классу.

Декораторы контроллера

import { Controller, Get, Post, Body, Param, ParseIntPipe } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './entities/user.entity';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll(): User[] {
    return this.usersService.findAll();
  }

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number): User | undefined {
    return this.usersService.findOne(id);
  }

  @Post()
  create(@Body() createUserDto: CreateUserDto): User {
    return this.usersService.create(createUserDto);
  }
}

ParseIntPipe здесь — это пример использования встроенных пайпов NestJS, которые преобразуют строковый параметр из URL в число. Тип параметра id: number корректно отражает, что после пайпа значение гарантированно является числом.

DTO: классы вместо интерфейсов

DTO (Data Transfer Object) — объекты для описания входящих данных. NestJS рекомендует использовать классы, а не интерфейсы — и это принципиальный момент.

Интерфейсы TypeScript исчезают после компиляции. Классы остаются в JavaScript и доступны в runtime. Это позволяет библиотеке class-validator проверять данные на основе декораторов, а class-transformer — преобразовывать plain-объекты в экземпляры класса.

import { IsString, IsEmail, IsInt, Min, Max, IsOptional } from 'class-validator';

export class CreateUserDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;

  @IsInt()
  @Min(0)
  @Max(120)
  age: number;

  @IsOptional()
  @IsString()
  bio?: string;
}

Чтобы NestJS автоматически валидировал все входящие DTO, нужно подключить ValidationPipe глобально:

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  await app.listen(3000);
}

bootstrap();

Опция transform: true активирует автоматическое преобразование типов: строки из запроса будут конвертироваться в числа, булевы значения и т.д. — в соответствии с типами, указанными в DTO.

PartialType и другие mapped types

NestJS предоставляет утилиты для создания производных DTO:

import { PartialType, PickType, OmitType, IntersectionType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

// Все поля становятся необязательными
export class UpdateUserDto extends PartialType(CreateUserDto) {}

// Только указанные поля
export class LoginDto extends PickType(CreateUserDto, ['email'] as const) {}

// Все поля кроме указанных
export class PublicUserDto extends OmitType(CreateUserDto, ['age'] as const) {}

PartialType не просто делает поля опциональными на уровне TypeScript — он копирует все декораторы валидации из исходного класса и оборачивает их в @IsOptional(). Это поведение недостижимо с обычными TypeScript utility types (Partial<T>), потому что те работают только с типами, но не с runtime-декораторами.

Дженерики в NestJS

Дженерики позволяют создавать переиспользуемые паттерны с сохранением типизации.

Стандартный ответ API

export class ApiResponse<T> {
  data: T;
  message: string;
  statusCode: number;

  constructor(data: T, message = 'Success', statusCode = 200) {
    this.data = data;
    this.message = message;
    this.statusCode = statusCode;
  }
}

Применение в контроллере:

@Get()
findAll(): ApiResponse<User[]> {
  const users = this.usersService.findAll();
  return new ApiResponse(users);
}

Дженерики в репозиториях

При работе с TypeORM или Prisma дженерики помогают строить базовые репозитории:

import { Repository, FindOptionsWhere, DeepPartial } from 'typeorm';

export abstract class BaseRepository<T extends { id: number }> {
  constructor(protected readonly repository: Repository<T>) {}

  findAll(): Promise<T[]> {
    return this.repository.find();
  }

  findOne(id: number): Promise<T | null> {
    return this.repository.findOneBy({ id } as FindOptionsWhere<T>);
  }

  create(data: DeepPartial<T>): Promise<T> {
    const entity = this.repository.create(data);
    return this.repository.save(entity);
  }
}

@Injectable()
export class UsersRepository extends BaseRepository<User> {
  constructor(
    @InjectRepository(User)
    repository: Repository<User>,
  ) {
    super(repository);
  }
}

Type-safe Dependency Injection

Внедрение через конструктор

Основной и рекомендуемый способ:

@Injectable()
export class OrdersService {
  constructor(
    private readonly usersService: UsersService,
    private readonly paymentsService: PaymentsService,
  ) {}
}

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

Кастомные провайдеры с токенами

Когда нужно внедрить значение не-класс (строку, объект конфигурации), используются токены:

export const CONFIG_TOKEN = 'APP_CONFIG';

export interface AppConfig {
  apiUrl: string;
  timeout: number;
  maxRetries: number;
}

// В модуле
@Module({
  providers: [
    {
      provide: CONFIG_TOKEN,
      useValue: {
        apiUrl: 'https://api.example.com',
        timeout: 5000,
        maxRetries: 3,
      } satisfies AppConfig,
    },
  ],
})
export class AppModule {}

// В сервисе
@Injectable()
export class ApiService {
  constructor(
    @Inject(CONFIG_TOKEN) private readonly config: AppConfig,
  ) {}

  getUrl(): string {
    return this.config.apiUrl;
  }
}

Ключевое слово satisfies (TypeScript 4.9+) проверяет соответствие объекта типу AppConfig без потери литерального типа значений.

Интерфейсы как контракты для провайдеров

export interface NotificationService {
  send(to: string, message: string): Promise<void>;
}

export const NOTIFICATION_SERVICE = 'NOTIFICATION_SERVICE';

@Injectable()
export class EmailNotificationService implements NotificationService {
  async send(to: string, message: string): Promise<void> {
    // отправка email
  }
}

@Injectable()
export class SmsNotificationService implements NotificationService {
  async send(to: string, message: string): Promise<void> {
    // отправка SMS
  }
}

// В модуле можно переключать реализацию без изменения потребителя
@Module({
  providers: [
    {
      provide: NOTIFICATION_SERVICE,
      useClass: EmailNotificationService,
    },
  ],
})
export class NotificationsModule {}

Типизация исключений и фильтров

NestJS предоставляет иерархию исключений, которую удобно расширять:

import { HttpException, HttpStatus } from '@nestjs/common';

export class UserNotFoundException extends HttpException {
  constructor(id: number) {
    super(`Пользователь с id ${id} не найден`, HttpStatus.NOT_FOUND);
  }
}

// Использование в сервисе
findOne(id: number): User {
  const user = this.users.find(u => u.id === id);
  if (!user) {
    throw new UserNotFoundException(id);
  }
  return user;
}

Filter для обработки кастомных исключений:

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost): void {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: exception.message,
    });
  }
}

Здесь host.switchToHttp() возвращает специфичный для HTTP контекст. Дженерик ctx.getResponse<Response>() сообщает TypeScript, что речь идёт о типе из express, и включает автодополнение.

Типизация конфигурации с ConfigService

Модуль @nestjs/config поддерживает дженерик для строгой типизации:

import { ConfigService } from '@nestjs/config';

interface EnvironmentVariables {
  DATABASE_URL: string;
  JWT_SECRET: string;
  PORT: number;
}

@Injectable()
export class DatabaseService {
  constructor(
    private configService: ConfigService<EnvironmentVariables>,
  ) {}

  getConnectionString(): string {
    // TypeScript знает, что DATABASE_URL — строка
    return this.configService.get('DATABASE_URL', { infer: true });
  }
}

С опцией { infer: true } TypeScript выводит тип возвращаемого значения из интерфейса EnvironmentVariables, исключая необходимость явных приведений типов.

Пайпы с дженериками

Пайпы трансформируют или валидируют входные данные. Интерфейс PipeTransform параметризован:

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class ParsePositiveIntPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10);
    if (isNaN(val) || val <= 0) {
      throw new BadRequestException(
        `${metadata.data} должен быть положительным целым числом`,
      );
    }
    return val;
  }
}

PipeTransform<string, number> явно указывает: на вход приходит строка, на выходе — число. TypeScript проследит за корректностью реализации метода transform.

Итог

TypeScript в NestJS — это не просто синтаксический сахар. Вот что TypeScript даёт фреймворку на практике:

  • Декораторы и метаданные — основа механизма dependency injection
  • Классы вместо интерфейсов для DTO — позволяет библиотекам валидации работать в runtime
  • Дженерики — переиспользуемые паттерны без потери типобезопасности
  • Строгая типизация провайдеров — ошибки разрешения зависимостей обнаруживаются на этапе компиляции
  • Mapped types — производные DTO с копированием декораторов валидации

Чем строже настроен tsconfig.json (особенно strict: true), тем больше ошибок TypeScript находит до запуска кода. В контексте NestJS это особенно ценно, поскольку многие проблемы DI проявляются только в runtime.

Для глубокого погружения в разработку серверных приложений на NestJS с TypeScript — курс по Node.js на PurpleSchool.

Декораторы в TypeScript — полное руководствоСтрелочка вправо

Постройте личный план изучения Typescript до уровня Middle — бесплатно!

Typescript — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по Typescript

Переменные и константы в TypeScriptTypeScript: ключевое слово using и Explicit Resource ManagementНастройка типов с declare module в TypeScriptTypeScript типизация в Node.js проектеTypeScript ESLint — настройка линтера для проектаTypeScript 5: новые возможности и измененияПреобразование к типу в TypeScript (Type Assertion)Кортежи в TypeScripttsconfig.json — полный разбор настроек TypeScript компилятораTypeScript с tRPC — типобезопасный APITypeScript strict и strictNullChecks — строгий режимКак собрать TypeScript проект с RollupОбъекты в TypeScriptMonorepo с Turborepo и TypeScriptФункции в TypeScriptТип функции и стрелочные функции в TypeScriptТипизация ошибок и обработка исключений в TypeScriptTypeScript ключевое слово declareМассивы в TypeScript
Как правильно использовать тип UnknownИспользование Union в TypeScriptTypeScript с GraphQL — типизация и кодогенерацияТипы данных в TypeScriptType Guards в TypeScript — как работает сужение типовSymbols в TypeScriptСтруктурная типизация в TypeScriptNull и Undefined в TypeScriptТип never в TypeScriptСужение типов (Narrowing) в TypeScriptПонимание Literal Types. Ключевые аспекты и примеры использованияОператор infer в Typescript?Типы функций в TypeScript. Руководство для разработчиковTypeScript с Express.js: типизация маршрутов и middlewareEnums в TypeScriptДискриминированные объединения в TypeScriptСлужебный тип Awaited в TypeScriptКак типизировать массивы TypeScriptКак правильно использовать тип any
Открыть базу знаний

Лучшие курсы по теме

изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

CSS Flexbox

Антон Ларичев
Гарантия
Бонусы
иконка звёздочки рейтинга4.9
бесплатно
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий