di-wise
v0.2.6
Published
Lightweight and flexible dependency injection library for JavaScript and TypeScript, w/wo ECMAScript decorators.
Downloads
731
Maintainers
Readme
di-wise 🧙♀️
Lightweight and flexible dependency injection library for JavaScript and TypeScript, w/wo ECMAScript decorators.
Table of Contents
- 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
Installation
npm install di-wise
pnpm add di-wise
yarn add di-wise
Also available on JSR:
deno add jsr:@exuanbo/di-wise
Features
Zero dependencies
- No need for
reflect-metadata
- No TypeScript legacy
experimentalDecorators
required
Modern decorator implementation
- Built on ECMAScript Stage 3 Decorators: tc39/proposal-decorators
- Native support in TypeScript 5.0+, swc 1.3.47+, and esbuild 0.21.0+
Context-based DI system
- 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(); // => 🔥
Multiple provider types
- 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(); // => 👻
Hierarchical injection
- 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(); // => 🐍
Full control over registration and caching
- Explicit container management without global state
- Fine-grained control over instance lifecycle
- Transparent registry access for testing
Various injection scopes
- 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);
Inherited (default)
Inherits the scope from its dependent. If there is no dependent (top-level resolution), behaves like Transient
.
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);
Transient
Creates a new instance every time the dependency is requested. No caching occurs.
Resolution
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.
@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);
Container
Creates one instance per container (singleton pattern). The instance is cached and reused for all subsequent resolutions within the same container.
Flexible token-based injection
- 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
}
Automatic circular dependency resolution
- 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);
Dynamic injection
- 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);
Constructor Injection
See discussion Does di-wise support constructor injection? #12
Middleware
- 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.
Usage
🏗️ WIP (PR welcome)
API
See API documentation.
Credits
Inspired by:
License
MIT License @ 2024-Present Xuanbo Cheng