Lightweight and flexible dependency injection library for JavaScript and TypeScript, w/wo ECMAScript decorators.
- di-wise π§ββοΈ
- Table of Contents
- Installation
- Features
- Zero dependencies
- Modern decorator implementation
- Context-based DI system
- Multiple provider types
- Hierarchical injection
- Full control over registration and caching
- Various injection scopes
- Flexible token-based injection
- Automatic circular dependency resolution
- Dynamic injection
- Constructor Injection
- Middleware
- Usage
- API
- Credits
- License
npm install di-wise
pnpm add di-wise
yarn add di-wise
Also available on JSR:
deno add jsr:@exuanbo/di-wise
- No need for
reflect-metadata
- No TypeScript legacy
experimentalDecorators
required
- Built on ECMAScript Stage 3 Decorators: tc39/proposal-decorators
- Native support in TypeScript 5.0+, swc 1.3.47+, and esbuild 0.21.0+
- Flexible decorator-based or function-based injection
- Full type inference support β¨
- Optional decorators with equivalent function alternatives
Example:
import {createContainer, Inject, inject, Injectable, Scope, Scoped, Type} from "di-wise";
interface Spell {
cast(): void;
}
const Spell = Type<Spell>("Spell");
@Scoped(Scope.Container)
@Injectable(Spell)
class Fireball implements Spell {
cast() {
console.log("π₯");
}
}
class Wizard {
@Inject(Wand)
wand!: Wand;
// Equivalent to
wand = inject(Wand);
constructor(spell = inject(Spell)) {
// inject() can be used anywhere during construction
this.wand.store(spell);
}
}
const container = createContainer();
container.register(Fireball);
// Under the hood
[Fireball, Spell].forEach((token) => {
container.register(
token,
{useClass: Fireball},
{scope: Scope.Container},
);
});
const wizard = container.resolve(Wizard);
wizard.wand.activate(); // => π₯
- Class, Factory, and Value providers
- Built-in helpers for one-off providers:
Build()
,Value()
- Seamless integration with existing classes
Example:
import {Build, createContainer, inject, Value} from "di-wise";
class Wizard {
equipment = inject(
Cloak,
// Provide a default value
Value({
activate() {
console.log("π»");
},
}),
);
wand: Wand;
constructor(wand: Wand) {
this.wand = wand;
}
}
const container = createContainer();
const wizard = container.resolve(
Build(() => {
// inject() can be used in factory functions
const wand = inject(Wand);
return new Wizard(wand);
}),
);
wizard.equipment.activate(); // => π»
- Parent-child container relationships
- Automatic token resolution through the container hierarchy
- Isolated registration with shared dependencies
Example:
import {createContainer, inject, Injectable, Type} from "di-wise";
const MagicSchool = Type<string>("MagicSchool");
const Spell = Type<{cast(): void}>("Spell");
// Parent container with shared config
const hogwarts = createContainer();
hogwarts.register(MagicSchool, {useValue: "Hogwarts"});
@Injectable(Spell)
class Fireball {
school = inject(MagicSchool);
cast() {
console.log(`π₯ from ${this.school}`);
}
}
// Child containers with isolated spells
const gryffindor = hogwarts.createChild();
gryffindor.register(Fireball);
const slytherin = hogwarts.createChild();
slytherin.register(Spell, {
useValue: {cast: () => console.log("π")},
});
gryffindor.resolve(Spell).cast(); // => π₯ from Hogwarts
slytherin.resolve(Spell).cast(); // => π
- Explicit container management without global state
- Fine-grained control over instance lifecycle
- Transparent registry access for testing
- Flexible scoping system:
Inherited
(default),Transient
,Resolution
,Container
- Smart scope resolution for dependencies
- Configurable default scopes per container
Example for singleton pattern:
import {createContainer, Scope} from "di-wise";
export const singletons = createContainer({
defaultScope: Scope.Container,
autoRegister: true,
});
// Always resolves to the same instance
const wizard = singletons.resolve(Wizard);
Inherits the scope from its dependent. If there is no dependent (top-level resolution), behaves like Transient
.
Example
import {createContainer, Scope, Scoped} from "di-wise";
@Scoped(Scope.Container)
class Wizard {
wand = inject(Wand);
}
const container = createContainer();
container.register(
Wand,
{useClass: Wand},
{scope: Scope.Inherited},
);
container.register(Wizard);
// Dependency Wand will be resolved with "Container" scope
const wizard = container.resolve(Wizard);
Creates a new instance every time the dependency is requested. No caching occurs.
Creates one instance per resolution graph. The same instance will be reused within a single dependency resolution, but new instances are created for separate resolutions.
Example
@Scoped(Scope.Resolution)
class Wand {}
class Inventory {
wand = inject(Wand);
}
class Wizard {
inventory = inject(Inventory);
wand = inject(Wand);
}
const container = createContainer();
const wizard = container.resolve(Wizard);
expect(wizard.inventory.wand).toBe(wizard.wand);
Creates one instance per container (singleton pattern). The instance is cached and reused for all subsequent resolutions within the same container.
- Multiple token resolution with union type inference β¨
- Support for optional dependencies via
Type.Null
andType.Undefined
- Interface-based token system
Example:
import {inject, Type} from "di-wise";
class Wizard {
wand = inject(Wand, Type.Null);
// ^? (property) Wizard.wand: Wand | null
spells = injectAll(Spell, Type.Null);
// ^? (property) Wizard.spells: Spell[]
// => []
}
- Smart handling of circular dependencies
- Multiple resolution strategies (
@Inject()
orinject.by()
) - Maintains type safety
Example:
import {createContainer, Inject, inject} from "di-wise";
class Wand {
owner = inject(Wizard);
}
class Wizard {
@Inject(Wand)
wand!: Wand;
// Equivalent to
wand = inject.by(this, Wand);
}
const container = createContainer();
const wizard = container.resolve(Wizard);
expect(wizard.wand.owner).toBe(wizard);
- On-demand dependency resolution via
Injector
- Context-aware lazy loading
- Preserves proper scoping and circular dependency handling
Example:
import {createContainer, inject, Injector} from "di-wise";
class Wizard {
private injector = inject(Injector);
private wand?: Wand;
getWand() {
// Lazy load wand only when needed
return (this.wand ??= this.injector.inject(Wand));
}
castAllSpells() {
// Get all registered spells
const spells = this.injector.injectAll(Spell);
spells.forEach((spell) => spell.cast());
}
}
const container = createContainer();
const wizard = container.resolve(Wizard);
wizard.getWand(); // => Wand
The injector maintains the same resolution context as its injection point, allowing proper handling of scopes and circular dependencies:
import {createContainer, inject, Injector} from "di-wise";
class Wand {
owner = inject(Wizard);
}
class Wizard {
private injector = inject.by(this, Injector);
getWand() {
return this.injector.inject(Wand);
}
}
const container = createContainer();
const wizard = container.resolve(Wizard);
const wand = wizard.getWand();
expect(wand.owner).toBe(wizard);
See discussion Does di-wise support constructor injection? #12
- Extensible container behavior through middleware
- Composable middleware chain with predictable execution order
- Full access to container lifecycle
Example:
import {applyMiddleware, createContainer, type Middleware} from "di-wise";
const logger: Middleware = (composer, _api) => {
composer
.use("resolve", (next) => (token) => {
console.log("Resolving:", token.name);
const result = next(token);
console.log("Resolved:", token.name);
return result;
})
.use("resolveAll", (next) => (token) => {
console.log("Resolving all:", token.name);
const result = next(token);
console.log("Resolved all:", token.name);
return result;
});
};
const performanceTracker: Middleware = (composer, _api) => {
composer.use("resolve", (next) => (token) => {
const start = performance.now();
const result = next(token);
const end = performance.now();
console.log(`Resolution time for ${token.name}: ${end - start}ms`);
return result;
});
};
const container = applyMiddleware(createContainer(), [logger, performanceTracker]);
// Use the container with applied middlewares
const wizard = container.resolve(Wizard);
Middlewares are applied in array order but execute in reverse order, allowing outer middlewares to wrap and control the behavior of inner middlewares.
ποΈ WIP (PR welcome)
See API documentation.
Inspired by:
MIT License @ 2024-Present Xuanbo Cheng