Dependency Injection в TypeScript: Полное руководство с примерами

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

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

Начать курс

Что такое Dependency Injection в TypeScript и зачем это нужно?

Dependency Injection (DI) — это паттерн проектирования, реализующий принцип инверсии управления (Inversion of Control, IoC). Вместо того чтобы объект сам создавал свои зависимости, они "внедряются" извне. В TypeScript этот паттерн особенно полезен благодаря строгой типизации и возможностям декораторов.

Представьте, что вы строите приложение для интернет-магазина. У вас есть класс OrderService, который должен отправлять уведомления. Без DI он будет жестко привязан к конкретному способу отправки (например, Email). С DI вы можете легко подменить Email на SMS или Push-уведомления, не меняя код OrderService.

Почему стоит использовать DI в TypeScript?

  • Тестируемость: Легко подменять реальные сервисы на mock-объекты в юнит-тестах.
  • Гибкость: Система становится модульной, зависимости можно менять без изменения клиентского кода.
  • Упрощение поддержки: Каждый класс отвечает только за свою бизнес-логику, а не за создание других объектов.
  • Типобезопасность: TypeScript проверяет типы на этапе компиляции, что уменьшает количество ошибок во время выполнения.

Базовая реализация DI в TypeScript без фреймворков

Начнем с простого примера. Создадим интерфейс для сервиса уведомлений и две его реализации.

// Определяем интерфейс
interface INotificationService {
    send(message: string): void;
}

// Реализация для Email
class EmailNotificationService implements INotificationService {
    send(message: string): void {
        console.log(`Отправка Email: ${message}`);
    }
}

// Реализация для SMS
class SMSNotificationService implements INotificationService {
    send(message: string): void {
        console.log(`Отправка SMS: ${message}`);
    }
}

// Класс, который использует DI
class OrderService {
    constructor(private notificationService: INotificationService) {}

    processOrder(orderId: number): void {
        // Логика обработки заказа
        this.notificationService.send(`Заказ #${orderId} обработан`);
    }
}

// Использование
const emailService = new EmailNotificationService();
const orderService = new OrderService(emailService);
orderService.processOrder(123); // Отправка Email: Заказ #123 обработан

В этом примере OrderService не знает, какой именно сервис уведомлений используется. Он просто вызывает метод send через интерфейс.

Продвинутый DI: Создание простого IoC контейнера

Для больших проектов ручное внедрение зависимостей становится громоздким. Здесь на помощь приходит IoC-контейнер. Рассмотрим, как реализовать его с использованием декораторов TypeScript.

Шаг 1: Включите поддержку декораторов в tsconfig.json

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

Шаг 2: Создайте DI контейнер

// di-container.ts
type Constructor<T> = new (...args: any[]) => T;

class DIContainer {
    private services = new Map<string, any>();

    register<T>(token: string, implementation: Constructor<T>): void {
        this.services.set(token, implementation);
    }

    resolve<T>(token: string): T {
        const Implementation = this.services.get(token);
        if (!Implementation) {
            throw new Error(`Сервис "${token}" не зарегистрирован`);
        }

        // Получаем типы параметров конструктора через рефлексию
        const paramTypes: Constructor<any>[] = 
            Reflect.getMetadata('design:paramtypes', Implementation) || [];

        // Рекурсивно разрешаем зависимости
        const dependencies = paramTypes.map((param) => {
            const paramName = param.name;
            return this.resolve(paramName);
        });

        return new Implementation(...dependencies);
    }
}

export const container = new DIContainer();

Шаг 3: Используйте декоратор для автоматической регистрации

// decorators.ts
function Injectable() {
    return function (target: any) {
        // Автоматически регистрируем класс в контейнере
        container.register(target.name, target);
    };
}

// Пример использования
@Injectable()
class LoggerService {
    log(message: string): void {
        console.log(`[LOG]: ${message}`);
    }
}

@Injectable()
class UserService {
    constructor(private logger: LoggerService) {}

    getUser(id: number): void {
        this.logger.log(`Запрос пользователя с ID: ${id}`);
        // Логика получения пользователя
    }
}

// Регистрируем контейнер в глобальной области
import { container } from './di-container';

// Разрешаем зависимости
const userService = container.resolve<UserService>('UserService');
userService.getUser(42); // [LOG]: Запрос пользователя с ID: 42

Практические советы по использованию DI в TypeScript

1. Используйте символы или строковые константы для токенов

Избегайте магических строк. Лучше определить константы:

export const TOKENS = {
    LoggerService: Symbol('LoggerService'),
    UserService: Symbol('UserService')
};

2. Разделяйте интерфейсы и реализации

Храните интерфейсы в отдельной папке interfaces/, а реализации — в services/. Это облегчает подмену зависимостей.

3. Используйте фабрики для сложных зависимостей

Если создание объекта требует дополнительной логики, используйте фабричный метод:

container.register('DatabaseConnection', () => {
    const config = loadConfig();
    return new DatabaseConnection(config.host, config.port);
});

4. Не злоупотребляйте DI

DI отлично подходит для сервисов, репозиториев и утилит. Но не стоит внедрять примитивные типы или простые DTO (Data Transfer Objects).

Сравнение DI в TypeScript с другими подходами

Подход Плюсы Минусы
Ручное внедрение Простота, нет зависимостей Много boilerplate кода
DI контейнер (свой) Полный контроль, легковесность Нужно писать и поддерживать
Фреймворки (NestJS, In

Блоги

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