WireDI - Wire your dependency injection with type safety and compile-time validation
npm install @djodjonx/wiredi

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
- Full API Documentation - Complete TypeDoc API reference
- Getting Started Guide - Start using WireDI in 5 minutes
- Examples - Real-world integration examples
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:
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.``typescript`
// ❌ 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
`bashWith npm
npm install @djodjonx/wiredi
$3
@djodjonx/wiredi supports multiple containers. Install the one of your choice:`bash
Option 1: tsyringe (recommended)
npm install tsyringe reflect-metadataOption 2: Awilix
npm install awilixOption 3: InversifyJS
npm install inversify reflect-metadata
`Quick Start
$3
With tsyringe (recommended)
`typescript
// main.ts
import 'reflect-metadata'
import { container, Lifecycle } from 'tsyringe'
import { useContainerProvider, TsyringeProvider } from '@djodjonx/wiredi'useContainerProvider(new TsyringeProvider({ container, Lifecycle }))
`
With Awilix
`typescript
// main.ts
import * as awilix from 'awilix'
import { useContainerProvider, AwilixProvider } from '@djodjonx/wiredi'useContainerProvider(AwilixProvider.createSync(awilix, {
injectionMode: 'PROXY', // or 'CLASSIC'
}))
`
With InversifyJS
`typescript
// main.ts
import 'reflect-metadata'
import * as inversify from 'inversify'
import { useContainerProvider, InversifyProvider } from '@djodjonx/wiredi'useContainerProvider(InversifyProvider.createSync(inversify))
`$3
`typescript
// 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
}
// 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
`$3
`typescript
// 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
})
`
`$3
`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)
`IDE Plugin for Real-Time Validation
The TypeScript Language Service plugin detects configuration errors directly in your IDE.
$3
1. Add the plugin to your
tsconfig.json:`json
{
"compilerOptions": {
"plugins": [
{
"name": "@djodjonx/wiredi/plugin"
}
]
}
}
`2. 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:
- Settings → Languages & Frameworks → TypeScript
- Ensure TypeScript points to
node_modules/typescript
- Check "Use TypeScript Language Service"
- Restart the IDE$3
| 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 |$3
`typescript
// ❌ 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.
$3
`json
{
"compilerOptions": {
"plugins": [
{
"name": "@djodjonx/wiredi/plugin",
"verbose": true // Enable debug logs
}
]
}
}
`Injection Types
$3
`typescript
{ token: UserService }
`$3
`typescript
{ token: TOKENS.Logger, provider: ConsoleLogger }
`$3
`typescript
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 |$3
`typescript
{ token: TOKENS.ApiUrl, value: (context) => 'https://api.example.com' }
`$3
`typescript
{
token: TOKENS.HttpClient,
factory: (provider) => new HttpClient(provider.resolve(TOKENS.ApiUrl))
}
`Partials System
Partials allow you to reuse configurations across multiple builders:
`typescript
// 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 },
],
})
`$3
Important: Each token must be unique across all partials and the main configuration.
`typescript
// ❌ 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:
`typescript
// ✅ 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 },
],
})
`$3
Similar to tokens, each (event, listener) pair must be unique across all partials and the main configuration:
`typescript
// ❌ 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
],
})
`Event Programming
> Note: The
listeners property is optional. If your application doesn't use events, you can omit it entirely from your configuration.$3
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.$3
WireDI allows you to bind events to listeners declaratively:$3
First, configure the EventDispatcherProvider at startup (after the container provider):`typescript
import {
useEventDispatcherProvider,
MutableEventDispatcherProvider,
getContainerProvider
} from '@djodjonx/wiredi'useEventDispatcherProvider(new MutableEventDispatcherProvider({
containerProvider: getContainerProvider(),
}))
`$3
Events are simple classes. Listeners are services that implement an onEvent method.`typescript
// 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!')
}
}
`$3
Bind them in your builder configuration using the listeners property:`typescript
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:
`typescript
import { getEventDispatcherProvider } from '@djodjonx/wiredi'getEventDispatcherProvider().dispatch(new UserCreatedEvent(newUser))
// -> SendWelcomeEmail.onEvent() is automatically called
`Creating a Custom Provider
To use an unsupported DI container, implement the
ContainerProvider interface:`typescript
import type { ContainerProvider, ProviderLifecycle } from '@djodjonx/wiredi'class MyCustomProvider implements ContainerProvider {
readonly name = 'my-provider'
registerValue(token: symbol, value: T): void { / ... / }
registerFactory(token: symbol, factory: (p: ContainerProvider) => T): void { / ... / }
registerClass(token: symbol | Constructor, impl?: Constructor, lifecycle?: ProviderLifecycle): void { / ... / }
isRegistered(token: symbol | Constructor): boolean { / ... / }
resolve(token: symbol | Constructor): T { / ... / }
createScope(): ContainerProvider { / ... / }
dispose(): void { / ... / }
getUnderlyingContainer(): unknown { / ... / }
}
`Full Examples
examples/ folder for comprehensive examples:$3
- tsyringe - Microsoft's lightweight DI container
- Awilix - Powerful proxy-based injection
- InversifyJS - Feature-rich IoC container$3
- RxJS Provider - Reactive programming
- EventEmitter Provider - Node.js built-in
- Priority Provider - Ordered workflowsSee: Examples Guide for detailed documentation and learning path.
API Reference
$3
`typescript
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)
`$3
`typescript
import {
useEventDispatcherProvider,
MutableEventDispatcherProvider,
getEventDispatcherProvider
} from '@djodjonx/wiredi'// Configuration
useEventDispatcherProvider(new MutableEventDispatcherProvider({
containerProvider: getContainerProvider(),
}))
// Dispatch events
getEventDispatcherProvider().dispatch(new UserCreatedEvent(user))
`Documentation
$3
Full API documentation is available online and can be generated locally:
Online: View API Documentation (GitHub Pages)
Generate locally:
`bash
pnpm docs
open docs/api/index.html
`$3
- Quick Start Guide - Get started in 4 steps
- Plugin Installation - IDE integration
- Provider Examples - Integration with tsyringe, Awilix, InversifyJS
Troubleshooting
$3
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$3
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