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();
// Мок для л