tsyringe для TypeScript: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

tsyringe для TypeScript: полное руководство с примерами

В современной разработке на TypeScript управление зависимостями является одной из ключевых задач, особенно при построении масштабируемых и тестируемых приложений. Библиотека tsyringe представляет собой легковесный и мощный контейнер внедрения зависимостей (Dependency Injection, DI), который полностью построен на декораторах TypeScript. Она позволяет разработчикам декларативно описывать зависимости между компонентами, автоматически разрешать их и управлять жизненным циклом объектов.

Зачем нужна tsyringe? В больших проектах ручное создание экземпляров классов и передача их конструкторам приводит к жесткой связанности кода, усложняет тестирование и затрудняет замену реализаций. tsyringe решает эти проблемы, предоставляя централизованный реестр (контейнер), который знает, как создавать и предоставлять зависимости. Вы просто указываете, какие классы или токены должны быть зарегистрированы, а контейнер автоматически внедряет их туда, где они нужны. Это делает код чище, модульнее и значительно упрощает написание unit-тестов.

Библиотека активно использует декораторы TypeScript (@injectable(), @inject()), что делает синтаксис интуитивно понятным для разработчиков, знакомых с Angular или NestJS. tsyringe не требует сложной конфигурации, работает "из коробки" с современными версиями TypeScript и поддерживает различные стратегии регистрации (singleton, transient, scoped). В этой статье мы подробно разберем все аспекты работы с tsyringe, начиная от установки и заканчивая сложными сценариями использования.

Установка

Для установки tsyringe в ваш проект на TypeScript выполните следующую команду с использованием npm:

npm install tsyringe reflect-metadata

После установки необходимо убедиться, что в вашем проекте настроена поддержка декораторов. Для этого в файле tsconfig.json должны быть включены следующие опции:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Также в точке входа вашего приложения (например, index.ts) необходимо импортировать reflect-metadata один раз, чтобы включить метаданные для декораторов:

import "reflect-metadata";
// ... остальной код

Быстрый старт: минимальный рабочий пример

Давайте создадим простое приложение, которое демонстрирует базовую механику работы tsyringe. У нас будет сервис для работы с базой данных, который внедряется в контроллер.

import "reflect-metadata";
import { container, injectable, inject } from "tsyringe";

// 1. Определяем сервис, который будет внедряться
@injectable()
class DatabaseService {
  getData(): string {
    return "Данные из базы данных";
  }
}

// 2. Определяем контроллер, который зависит от DatabaseService
@injectable()
class UserController {
  constructor(
    @inject(DatabaseService) private dbService: DatabaseService
  ) {}

  getUserData(): string {
    return this.dbService.getData();
  }
}

// 3. Регистрируем классы в контейнере (в данном случае это не обязательно,
//    так как tsyringe может автоматически разрешать зарегистрированные классы)
container.registerSingleton(DatabaseService);
container.registerSingleton(UserController);

// 4. Используем контейнер для получения экземпляра контроллера
const controller = container.resolve(UserController);
console.log(controller.getUserData()); // Выведет: "Данные из базы данных"

Этот пример показывает три ключевых шага: пометить класс как @injectable(), указать зависимости через @inject() в конструкторе и получить экземпляр через container.resolve(). Обратите внимание, что если класс зарегистрирован с помощью декоратора @injectable(), tsyringe может автоматически его разрешить без явной регистрации, но для явного управления жизненным циклом рекомендуется использовать методы регистрации.

Основные методы, функции и классы

Ниже приведен подробный разбор 8 ключевых элементов API библиотеки tsyringe.

1. @injectable() (декоратор класса)

Сигнатура: @injectable()

Что делает: Помечает класс как доступный для внедрения зависимостей. Без этого декоратора контейнер не сможет создать экземпляр класса.

import { injectable } from "tsyringe";

@injectable()
class Logger {
  log(message: string): void {
    console.log(`[LOG]: ${message}`);
  }
}

2. @inject(token) (декоратор параметра)

Сигнатура: @inject(token: InjectionToken<T>)

Что делает: Указывает контейнеру, какой именно токен (класс, строка или Symbol) должен быть внедрен в данный параметр конструктора. Используется, когда тип параметра не может быть автоматически определен (например, при использовании интерфейсов или примитивов).

import { injectable, inject } from "tsyringe";

interface IConfig {
  apiUrl: string;
}

const CONFIG_TOKEN = "CONFIG";

@injectable()
class ApiService {
  constructor(@inject(CONFIG_TOKEN) private config: IConfig) {}

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

3. container.register(token, provider)

Сигнатура: container.register(token: InjectionToken<T>, provider: Provider<T>): void

Что делает: Регистрирует провайдер (класс, фабрику или значение) для указанного токена. Это основной способ сообщить контейнеру, как создавать или получать зависимость.

import { container } from "tsyringe";

class Config {
  apiUrl = "https://api.example.com";
}

// Регистрируем класс как singleton
container.register(Config, { useClass: Config });

// Регистрируем значение
container.register("API_URL", { useValue: "https://api.example.com" });

// Регистрируем фабрику
container.register("RANDOM_NUMBER", {
  useFactory: () => Math.random()
});

4. container.registerSingleton(token)

Сигнатура: container.registerSingleton(token: InjectionToken<T>): void

Что делает: Регистрирует класс как singleton. Это означает, что контейнер будет создавать только один экземпляр класса и возвращать его при каждом запросе. Удобно для сервисов, которые должны быть общими (например, логгер, кэш).

import { container } from "tsyringe";

@injectable()
class CacheService {
  private cache = new Map();

  set(key: string, value: any): void {
    this.cache.set(key, value);
  }

  get(key: string): any {
    return this.cache.get(key);
  }
}

container.registerSingleton(CacheService);

// Оба вызова вернут один и тот же экземпляр
const cache1 = container.resolve(CacheService);
const cache2 = container.resolve(CacheService);
console.log(cache1 === cache2); // true

5. container.resolve(token)

Сигнатура: container.resolve<T>(token: InjectionToken<T>): T

Что делает: Разрешает зависимость для указанного токена. Контейнер создает (или возвращает уже существующий) экземпляр класса, автоматически внедряя все его зависимости. Это основной способ получения объектов из контейнера.

import { container } from "tsyringe";

@injectable()
class DatabaseConnection {
  connect(): void {
    console.log("Подключение к БД...");
  }
}

@injectable()
class UserRepository {
  constructor(private db: DatabaseConnection) {}

  findUser(id: number): string {
    this.db.connect();
    return `Пользователь с ID ${id}`;
  }
}

// Контейнер автоматически создаст DatabaseConnection и передаст его в UserRepository
const repo = container.resolve(UserRepository);
console.log(repo.findUser(1));

6. container.registerType(from, to)

Сигнатура: container.registerType<T>(from: InjectionToken<T>, to: InjectionToken<T>): void

Что делает: Регистрирует псевдоним (alias) для токена. Позволяет сказать контейнеру: "когда запрашивают токен from, используй реализацию, зарегистрированную для токена to". Это полезно для абстракций и интерфейсов.

import { container } from "tsyringe";

// Абстрактный класс или интерфейс (в TypeScript интерфейсы не работают с декораторами,
// поэтому используем абстрактный класс или строковый токен)
abstract class ILogger {
  abstract log(msg: string): void;
}

@injectable()
class ConsoleLogger implements ILogger {
  log(msg: string): void {
    console.log(msg);
  }
}

// Регистрируем реализацию
container.registerSingleton(ConsoleLogger);

// Создаем псевдоним: ILogger -> ConsoleLogger
container.registerType(ILogger, ConsoleLogger);

// Теперь можно запрашивать ILogger, и контейнер вернет ConsoleLogger
const logger = container.resolve(ILogger);
logger.log("Hello from alias!");

7. container.clearInstances()

Сигнатура: container.clearInstances(): void

Что делает: Очищает все созданные экземпляры singleton-зависимостей в контейнере. Полезно для сброса состояния между тестами или при перезагрузке модулей.

import { container } from "tsyringe";

@injectable()
class AppState {
  public counter = 0;
}

container.registerSingleton(AppState);

const state1 = container.resolve(AppState);
state1.counter = 10;

// Очищаем все singleton-экземпляры
container.clearInstances();

const state2 = container.resolve(AppState);
console.log(state2.counter); // 0, так как это новый экземпляр
console.log(state1 === state2); // false

8. container.isRegistered(token)

Сигнатура: container.isRegistered(token: InjectionToken<T>): boolean

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

import { container } from "tsyringe";

@injectable()
class MyService {}

container.registerSingleton(MyService);

console.log(container.isRegistered(MyService)); // true
console.log(container.isRegistered("NonExistent")); // false

// Безопасное разрешение
if (container.isRegistered(MyService)) {
  const service = container.resolve(MyService);
  // работаем с service
}

Реальный практический пример: система уведомлений

Рассмотрим более сложный пример — систему отправки уведомлений, которая поддерживает разные каналы (Email, SMS) и использует логгирование. Мы покажем, как tsyringe помогает организовать гибкую и тестируемую архитектуру.

import "reflect-metadata";
import { container, injectable, inject, singleton } from "tsyringe";

// --- Абстракции ---
interface INotificationChannel {
  send(recipient: string, message: string): Promise;
}

interface ILogger {
  info(msg: string): void;
  error(msg: string): void;
}

// --- Реализации ---
@injectable()
class ConsoleLogger implements ILogger {
  info(msg: string): void {
    console.log(`[INFO] ${new Date().toISOString()}: ${msg}`);
  }

  error(msg: string): void {
    console.error(`[ERROR] ${new Date().toISOString()}: ${msg}`);
  }
}

@injectable()
class EmailChannel implements INotificationChannel {
  constructor(@inject("Logger") private logger: ILogger) {}

  async send(recipient: string, message: string): Promise {
    this.logger.info(`Отправка Email на ${recipient}: ${message}`);
    // Имитация отправки
    await new Promise(resolve => setTimeout(resolve, 100));
    this.logger.info(`Email успешно отправлен на ${recipient}`);
  }
}

@injectable()
class SMSChannel implements INotificationChannel {
  constructor(@inject("Logger") private logger: ILogger) {}

  async send(recipient: string, message: string): Promise {
    this.logger.info(`Отправка SMS на ${recipient}: ${message}`);
    await new Promise(resolve => setTimeout(resolve, 50));
    this.logger.info(`SMS успешно отправлено на ${recipient}`);
  }
}

// --- Основной сервис ---
@injectable()
class NotificationService {
  constructor(
    @inject("EmailChannel") private emailChannel: INotificationChannel,
    @inject("SMSChannel") private smsChannel: INotificationChannel,
    @inject("Logger") private logger: ILogger
  ) {}

  async sendEmail(recipient: string, message: string): Promise {
    this.logger.info(`Запрос на отправку Email пользователю ${recipient}`);
    await this.emailChannel.send(recipient, message);
  }

  async sendSMS(recipient: string, message: string): Promise {
    this.logger.info(`Запрос на отправку SMS пользователю ${recipient}`);
    await this.smsChannel.send(recipient, message);
  }
}

// --- Регистрация в контейнере ---
// Используем строковые токены для гибкости
container.registerSingleton("Logger", ConsoleLogger);
container.registerSingleton("EmailChannel", EmailChannel);
container.registerSingleton("SMSChannel", SMSChannel);
container.registerSingleton(NotificationService);

// --- Использование ---
async function main() {
  const notificationService = container.resolve(NotificationService);

  await notificationService.sendEmail("user@example.com", "Ваш заказ подтвержден!");
  await notificationService.sendSMS("+1234567890", "Код подтверждения: 1234");

  console.log("Все уведомления отправлены.");
}

main().catch(console.error);

// --- Пример тестирования с моками ---
// В тестах мы можем легко переопределить зависимости:
function testExample() {
  // Создаем дочерний контейнер для изоляции теста
  const testContainer = container.createChildContainer();

  // Мок для л                    

Рекомендаций