di-wise - v0.1.0

di-wise 🧙‍♀️

npm npm bundle size GitHub Workflow Status (with branch) Codecov (with branch)

Lightweight and flexible dependency injection library for JavaScript and TypeScript, w/wo ECMAScript decorators.

# npm
npm install di-wise

# Yarn
yarn add di-wise

# pnpm
pnpm add di-wise
  1. Zero dependencies:

  2. Modern decorator implementation:

  3. Full control over registration and caching:

    • No hidden root/global container or singleton scope
    • Exposed internal registry for testing and custom usage
  4. Context-based DI system inspired by Angular:

    • Use decorators @Injectable(), @Scoped(), @AutoRegister() on classes to define providers
    • Use decorators @Inject(), @InjectAll() on class fields to inject dependencies
    • Or use functions inject(), injectAll() with full type inference ✨

    Usage of decorators is optional:

    import {Container, Inject, inject, Injectable, Scope, Scoped, Type} from "di-wise";

    import {Wand} from "./weapons";

    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;

    // is equivalent to:
    wand = inject(Wand);

    constructor(spell = inject(Spell)) {
    // inject() can be used anywhere during construction
    this.wand.store(spell);
    }
    }

    const container = new Container();
    container.register(Fireball);

    // is equivalent to:
    [Fireball, Spell].forEach((token) => {
    container.register(
    token,
    {useClass: Fireball},
    {scope: Scope.Container},
    );
    });

    const wizard = container.resolve(Wizard);
    wizard.wand.activate(); // => 🔥
  5. Flexible token-based injection:

    • Use multiple tokens for resolving, with inferred type as a union ✨
    • Special tokens Type.Null and Type.Undefined for optional dependencies

    Example:

    import {inject, Type} from "di-wise";

    import {Wand} from "./weapons";

    class Wizard {
    wand = inject(Wand, Type.Null);
    // ^? (property) Wizard.wand: Wand | null
    }
  6. Various injection scopes:

    • Inherited (default), Transient, Resolution, and Container
    • Customizable default scope for containers

    Example:

    import {Container, Scope} from "di-wise";

    export const singletons = new Container({
    defaultScope: Scope.Container,
    autoRegister: true,
    });

    Inherited will be resolved as Transient for a top-level dependent.

    Resolution is similar to Transient, but the same instance will be reused during a single resolution tree.

  7. Automatic circular dependency resolution with @Inject() or inject.by():

    import {Container, Inject, inject} from "di-wise";

    class Wand {
    owner = inject(Wizard);
    }

    class Wizard {
    @Inject(Wand)
    wand!: Wand;

    // is equivalent to:
    wand = inject.by(this, Wand);
    }

    const container = new Container();
    const wizard = container.resolve(Wizard);

    expect(wizard.wand.owner).toBe(wizard);
  8. Multiple provider types:

    • ClassProvider, FactoryProvider, ValueProvider
    • Helper functions Build() and Value() for registering one-off providers

    Example:

    import {Build, Container, inject, Value} from "di-wise";

    import {Cloak} from "./equipments";
    import {Wand} from "./weapons";

    class Wizard {
    equipment = inject(
    Cloak,
    // fallback to a default value
    Value({
    activate() {
    console.log("👻");
    },
    }),
    );

    wand: Wand;

    constructor(wand: Wand) {
    this.wand = wand;
    }
    }

    const container = new Container();

    const wizard = container.resolve(
    Build(() => {
    // inject() can be used in factory functions
    const wand = inject(Wand);
    return new Wizard(wand);
    }),
    );

    wizard.equipment.activate(); // => 👻

🏗️ WIP (PR welcome)

See API documentation.

MIT License @ 2024-Present Xuanbo Cheng