@djodjonx/wiredi
    Preparing search index...

    @djodjonx/wiredi

    WireDI

    npm version Documentation CI TypeScript License: MIT

    Wire your dependency injection with type safety

    WireDI is a declarative, type-safe Dependency Injection builder. It eliminates "autowire" magic in favor of explicit, compile-time validated definitions, ensuring your application is free from missing dependencies and type mismatches.


    @djodjonx/wiredi allows you to:

    • Detect missing dependencies before runtime
    • Verify type consistency between interfaces and their implementations
    • Compose configurations with a reusable partials system
    • Switch DI containers without changing your business code

    Traditional DI containers (like tsyringe, InversifyJS) are powerful but often rely on runtime "magic" (autowiring) that can lead to:

    • 💥 Runtime errors when dependencies are missing.
    • 🐛 Silent failures when incorrect types are injected.

    WireDI solves this by shifting validation to compile-time:

    WireDI ignores autowiring in favor of explicit declarations. You define exactly what is injected. This ensures you never have a "missing dependency" error in production and your dependency graph is transparent.

    • Singleton by Default: All dependencies are registered as singletons, ensuring state consistency across your application.
    • Idempotent Builder: The useBuilder system prevents double registration. If multiple builders try to register the same token in the same container, WireDI respects the existing one. This allows you to safely compose overlapping modules without conflicts.

    Build your app like Lego. Create partial configurations for specific domains (e.g., Auth, Database, Logging) and extend them in your main application builder. This separation of concerns makes your config maintainable and testable.

    Errors are detected in your IDE instantly:

    // ❌ Error detected in IDE: "Logger" is not registered
    const config = defineBuilderConfig({
    builderId: 'app',
    injections: [
    { token: UserService }, // UserService depends on Logger
    ],
    // listeners is optional
    })

    Unlike traditional DI containers, WireDI's type checking works without decorators:

    • ✅ Type validation at configuration time, not runtime
    • ✅ Works with plain TypeScript classes
    • ✅ No need for @injectable or @inject decorators
    • ✅ Framework-agnostic type safety

    Learn more: Type Checking Without Decorators

    # With npm
    npm install @djodjonx/wiredi

    # With pnpm
    pnpm add @djodjonx/wiredi

    # With yarn
    yarn add @djodjonx/wiredi

    @djodjonx/wiredi supports multiple containers. Install the one of your choice:

    # Option 1: tsyringe (recommended)
    npm install tsyringe reflect-metadata

    # Option 2: Awilix
    npm install awilix

    # Option 3: InversifyJS
    npm install inversify reflect-metadata
    With tsyringe (recommended)
    // main.ts
    import 'reflect-metadata'
    import { container, Lifecycle } from 'tsyringe'
    import { useContainerProvider, TsyringeProvider } from '@djodjonx/wiredi'

    useContainerProvider(new TsyringeProvider({ container, Lifecycle }))
    With Awilix
    // main.ts
    import * as awilix from 'awilix'
    import { useContainerProvider, AwilixProvider } from '@djodjonx/wiredi'

    useContainerProvider(AwilixProvider.createSync(awilix, {
    injectionMode: 'PROXY', // or 'CLASSIC'
    }))
    With InversifyJS
    // main.ts
    import 'reflect-metadata'
    import * as inversify from 'inversify'
    import { useContainerProvider, InversifyProvider } from '@djodjonx/wiredi'

    useContainerProvider(InversifyProvider.createSync(inversify))
    // services.ts
    import { injectable, inject } from 'tsyringe' // or your container's decorators

    // Interfaces
    interface LoggerInterface {
    log(message: string): void
    }

    interface UserRepositoryInterface {
    findById(id: string): Promise<User | null>
    }

    // Implementations
    @injectable()
    class ConsoleLogger implements LoggerInterface {
    log(message: string) {
    console.log(`[LOG] ${message}`)
    }
    }

    @injectable()
    class UserRepository implements UserRepositoryInterface {
    async findById(id: string) {
    // ... implementation
    }
    }

    @injectable()
    class UserService {
    constructor(
    @inject(TOKENS.Logger) private logger: LoggerInterface,
    @inject(TOKENS.UserRepository) private repo: UserRepositoryInterface,
    ) {}

    async getUser(id: string) {
    this.logger.log(`Fetching user ${id}`)
    return this.repo.findById(id)
    }
    }

    // Injection tokens
    export const TOKENS = {
    Logger: Symbol('Logger'),
    UserRepository: Symbol('UserRepository'),
    } as const
    // config.ts
    import { defineBuilderConfig, definePartialConfig } from '@djodjonx/wiredi'

    // Reusable partial configuration
    const loggingPartial = definePartialConfig({
    injections: [
    { token: TOKENS.Logger, provider: ConsoleLogger },
    ],
    // listeners is optional - omit if you don't need event handling
    })

    // Main configuration
    export const appConfig = defineBuilderConfig({
    builderId: 'app.main',
    extends: [loggingPartial], // Inherits injections from partial
    injections: [
    { token: TOKENS.UserRepository, provider: UserRepository },
    { token: UserService }, // Class used as token
    ],
    // listeners is optional - only add if you need event handling
    })

    ### 4. Use the builder

    ```typescript
    // anywhere.ts
    import { useBuilder } from '@djodjonx/wiredi'
    import { appConfig } from './config'

    const { resolve } = useBuilder(appConfig)

    // Resolve dependencies with automatic typing
    const userService = resolve(UserService)
    const logger = resolve(TOKENS.Logger)

    The TypeScript Language Service plugin detects configuration errors directly in your IDE.

    1. Add the plugin to your tsconfig.json:
    {
    "compilerOptions": {
    "plugins": [
    {
    "name": "@djodjonx/wiredi/plugin"
    }
    ]
    }
    }
    1. Configure your IDE to use the project's TypeScript version:

    VS Code:

    • Cmd+Shift+P (Mac) or Ctrl+Shift+P (Windows/Linux)
    • Type "TypeScript: Select TypeScript Version"
    • Choose "Use Workspace Version"

    IntelliJ IDEA / WebStorm:

    • SettingsLanguages & FrameworksTypeScript
    • Ensure TypeScript points to node_modules/typescript
    • Check "Use TypeScript Language Service"
    • Restart the IDE
    Error Type Description Error Message
    🔴 Missing dependency A service requires an unregistered token [WireDI] Missing dependency: ...
    🔴 Type mismatch The provider doesn't implement the expected interface [WireDI] Type incompatible: ...
    🔴 Token collision Token already registered in a partial [WireDI] This token is already registered in a partial
    🔴 Duplicate listener Same (event, listener) pair registered twice [WireDI] Duplicate listener in the same configuration
    🔴 Listener collision Listener already registered in a partial [WireDI] This event listener is already registered in a partial
    // ❌ ERROR: ConsoleLogger doesn't implement UserRepositoryInterface
    const config = defineBuilderConfig({
    builderId: 'app',
    injections: [
    { token: TOKENS.UserRepository, provider: ConsoleLogger }, // Error here!
    ],
    // listeners is optional
    })

    The error appears on the provider line, even if it's defined in a separate partial file.

    {
    "compilerOptions": {
    "plugins": [
    {
    "name": "@djodjonx/wiredi/plugin",
    "verbose": true // Enable debug logs
    }
    ]
    }
    }
    { token: UserService }
    
    { token: TOKENS.Logger, provider: ConsoleLogger }
    
    import { ProviderLifecycle } from '@djodjonx/wiredi'

    { token: UserService, lifecycle: ProviderLifecycle.Transient }
    Lifecycle Description
    Singleton Single instance (default)
    Transient New instance on each resolution
    Scoped One instance per scope/request
    { token: TOKENS.ApiUrl, value: (context) => 'https://api.example.com' }
    
    {
    token: TOKENS.HttpClient,
    factory: (provider) => new HttpClient(provider.resolve(TOKENS.ApiUrl))
    }

    Partials allow you to reuse configurations across multiple builders:

    // partials/logging.ts
    export const loggingPartial = definePartialConfig({
    injections: [
    { token: TOKENS.Logger, provider: ConsoleLogger },
    ],
    })

    // partials/repositories.ts
    export const repositoriesPartial = definePartialConfig({
    injections: [
    { token: TOKENS.UserRepository, provider: PostgresUserRepository },
    { token: TOKENS.ProductRepository, provider: PostgresProductRepository },
    ],
    })

    // config.ts
    export const appConfig = defineBuilderConfig({
    builderId: 'app.main',
    extends: [loggingPartial, repositoriesPartial],
    injections: [
    { token: UserService },
    { token: ProductService },
    ],
    })

    Important: Each token must be unique across all partials and the main configuration.

    // ❌ ERROR: Token collision
    const loggingPartial = definePartialConfig({
    injections: [
    { token: TOKENS.Logger, provider: ConsoleLogger }
    ],
    })

    export const appConfig = defineBuilderConfig({
    builderId: 'app.main',
    extends: [loggingPartial],
    injections: [
    // ❌ This will cause a TypeScript error - token already defined in partial
    { token: TOKENS.Logger, provider: FileLogger }
    ],
    })

    For testing, create a separate configuration without the conflicting partial:

    // ✅ Correct approach for testing
    export const testConfig = defineBuilderConfig({
    builderId: 'app.test',
    extends: [], // Don't extend the partial with production logger
    injections: [
    { token: TOKENS.Logger, provider: MockLogger }, // ✅ OK - no collision
    { token: UserService },
    ],
    })

    Similar to tokens, each (event, listener) pair must be unique across all partials and the main configuration:

    // ❌ ERROR: Duplicate listener in the same configuration
    const config = defineBuilderConfig({
    builderId: 'app',
    injections: [],
    listeners: [
    { event: UserCreatedEvent, listener: EmailNotificationListener },
    { event: UserCreatedEvent, listener: EmailNotificationListener }, // ❌ Duplicate!
    ],
    })

    // ❌ ERROR: Listener already in partial
    const eventPartial = definePartialConfig({
    listeners: [
    { event: UserCreatedEvent, listener: EmailNotificationListener }
    ],
    })

    const config = defineBuilderConfig({
    builderId: 'app',
    extends: [eventPartial],
    injections: [],
    listeners: [
    { event: UserCreatedEvent, listener: EmailNotificationListener }, // ❌ Already in partial!
    ],
    })

    // ✅ OK: Different listener for the same event
    const validConfig = defineBuilderConfig({
    builderId: 'app',
    injections: [],
    listeners: [
    { event: UserCreatedEvent, listener: EmailNotificationListener },
    { event: UserCreatedEvent, listener: SmsNotificationListener }, // ✅ Different listener
    ],
    })

    Note: The listeners property is optional. If your application doesn't use events, you can omit it entirely from your configuration.

    In traditional event-driven architectures, event listeners are often scattered across the codebase (e.g., manual dispatcher.on(...) calls inside constructors or initialization scripts). This makes it hard to visualize the system's reactive flow.

    WireDI treats event listeners as part of your application's structural configuration. By declaring them alongside your dependency injections, you achieve:

    • 🔍 Visibility: See exactly who listens to what in a single configuration file.
    • 🧩 Decoupling: Services don't need to know about the dispatcher; they just implement onEvent.
    • 🛡️ Safety: Compile-time validation ensures your listener is compatible with the event.

    WireDI allows you to bind events to listeners declaratively:

    First, configure the EventDispatcherProvider at startup (after the container provider):

    import {
    useEventDispatcherProvider,
    MutableEventDispatcherProvider,
    getContainerProvider
    } from '@djodjonx/wiredi'

    useEventDispatcherProvider(new MutableEventDispatcherProvider({
    containerProvider: getContainerProvider(),
    }))

    Events are simple classes. Listeners are services that implement an onEvent method.

    // events/UserCreatedEvent.ts
    export class UserCreatedEvent {
    constructor(public readonly user: User) {}
    }

    // listeners/SendWelcomeEmail.ts
    export class SendWelcomeEmail {
    constructor(private mailer: MailerService) {}

    onEvent(event: UserCreatedEvent) {
    this.mailer.send(event.user.email, 'Welcome!')
    }
    }

    Bind them in your builder configuration using the listeners property:

    const appConfig = defineBuilderConfig({
    builderId: 'app',
    injections: [
    { token: SendWelcomeEmail }, // Register the listener itself
    { token: MailerService },
    ],
    listeners: [
    // Bind event -> listener
    { event: UserCreatedEvent, listener: SendWelcomeEmail },
    ],
    })

    Now, when you dispatch an event:

    import { getEventDispatcherProvider } from '@djodjonx/wiredi'

    getEventDispatcherProvider().dispatch(new UserCreatedEvent(newUser))
    // -> SendWelcomeEmail.onEvent() is automatically called

    To use an unsupported DI container, implement the ContainerProvider interface:

    import type { ContainerProvider, ProviderLifecycle } from '@djodjonx/wiredi'

    class MyCustomProvider implements ContainerProvider {
    readonly name = 'my-provider'

    registerValue<T>(token: symbol, value: T): void { /* ... */ }
    registerFactory<T>(token: symbol, factory: (p: ContainerProvider) => T): void { /* ... */ }
    registerClass<T>(token: symbol | Constructor<T>, impl?: Constructor<T>, lifecycle?: ProviderLifecycle): void { /* ... */ }
    isRegistered(token: symbol | Constructor): boolean { /* ... */ }
    resolve<T>(token: symbol | Constructor<T>): T { /* ... */ }
    createScope(): ContainerProvider { /* ... */ }
    dispose(): void { /* ... */ }
    getUnderlyingContainer(): unknown { /* ... */ }
    }

    Check the examples/ folder for comprehensive examples:

    • tsyringe - Microsoft's lightweight DI container
    • Awilix - Powerful proxy-based injection
    • InversifyJS - Feature-rich IoC container

    See: Examples Guide for detailed documentation and learning path.

    useContainerProvider(provider: ContainerProvider): void  // Configure the global provider
    getContainerProvider(): ContainerProvider // Get the provider
    hasContainerProvider(): boolean // Check if a provider is configured
    resetContainerProvider(): void // Reset (for tests)
    import {
    useEventDispatcherProvider,
    MutableEventDispatcherProvider,
    getEventDispatcherProvider
    } from '@djodjonx/wiredi'

    // Configuration
    useEventDispatcherProvider(new MutableEventDispatcherProvider({
    containerProvider: getContainerProvider(),
    }))

    // Dispatch events
    getEventDispatcherProvider().dispatch(new UserCreatedEvent(user))

    Full API documentation is available online and can be generated locally:

    Online: View API Documentation (GitHub Pages)

    Generate locally:

    pnpm docs
    open docs/api/index.html
    1. Verify that TypeScript uses the workspace version
    2. Restart the TypeScript server (Cmd+Shift+P → "TypeScript: Restart TS Server")
    3. Enable verbose mode to see logs

    TypeScript sees all Symbol() as the same type. To avoid type collisions with partials, use classes as tokens or define your tokens without as const.

    MIT