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:
Traditional DI containers (like tsyringe, InversifyJS) are powerful but often rely on runtime "magic" (autowiring) that can lead to:
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.
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:
@injectable or @inject decoratorsLearn 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
// main.ts
import 'reflect-metadata'
import { container, Lifecycle } from 'tsyringe'
import { useContainerProvider, TsyringeProvider } from '@djodjonx/wiredi'
useContainerProvider(new TsyringeProvider({ container, Lifecycle }))
// main.ts
import * as awilix from 'awilix'
import { useContainerProvider, AwilixProvider } from '@djodjonx/wiredi'
useContainerProvider(AwilixProvider.createSync(awilix, {
injectionMode: 'PROXY', // or 'CLASSIC'
}))
// 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.
tsconfig.json:{
"compilerOptions": {
"plugins": [
{
"name": "@djodjonx/wiredi/plugin"
}
]
}
}
VS Code:
Cmd+Shift+P (Mac) or Ctrl+Shift+P (Windows/Linux)IntelliJ IDEA / WebStorm:
node_modules/typescript| 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
listenersproperty 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:
onEvent.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:
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
Cmd+Shift+P → "TypeScript: Restart TS Server")verbose mode to see logsTypeScript 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