Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Lightweight and flexible dependency injection library for JavaScript and TypeScript, w/wo ECMAScript decorators
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
- 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
.
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);
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.
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);
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);
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
Add Package
deno add jsr:@exuanbo/di-wise
Import symbol
import * as _exuanbo_di_wise from "@exuanbo/di-wise";
---- OR ----
Import directly with a jsr specifier
import * as _exuanbo_di_wise from "jsr:@exuanbo/di-wise";
Add Package
npx jsr add @exuanbo/di-wise
Import symbol
import * as _exuanbo_di_wise from "@exuanbo/di-wise";
Add Package
yarn dlx jsr add @exuanbo/di-wise
Import symbol
import * as _exuanbo_di_wise from "@exuanbo/di-wise";
Add Package
pnpm dlx jsr add @exuanbo/di-wise
Import symbol
import * as _exuanbo_di_wise from "@exuanbo/di-wise";
Add Package
bunx jsr add @exuanbo/di-wise
Import symbol
import * as _exuanbo_di_wise from "@exuanbo/di-wise";