New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

wirebox

Package Overview
Dependencies
Maintainers
1
Versions
29
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

wirebox

A simple but flexible dependency injection library.

latest
Source
npmnpm
Version
0.7.0
Version published
Weekly downloads
34
385.71%
Maintainers
1
Weekly downloads
 
Created
Source

WireBox

A simple but flexible dependency injection library for TypeScript and JavaScript.

🚀 Features

  • Simple API: Easy-to-use and straightforward API.
  • Isolated Scopes: Class instances can be isolated into their own "scopes".
  • Type-Safe: Fully utilizes TypeScript's strong typing system.
  • Async Support: Write async class initializers.
  • Providers: Utility to inject non-class values (even async ones).
  • Decorators: Use optional type-safe TC39 decorators to simplify your code.

📦 Installation

Install the package using your favorite package manager:

npm install wirebox

🚀 How to use

Here is a basic example of how to use WireBox, for more detailed examples, please refer to the auto-generated documentation or see the examples directory.

1. Wiring classes

First of all, you need to configure how Wirebox should handle your classes. We will call this step "wiring". Classes which are not wired can not be used with Wirebox.

To wire your classes, you can use decorators or manual function calls. Both types of decorators are supported (TC39 stage 3 decorators proposal and legacy decorators) because we only rely on the first argument of the decorator, the class itself. But decorators are typed the new stage 3 proposal way.

Wiring options

Standalone

The simplest way to wire a class is to use the @standalone decorator. This decorator defines the class as standalone, which means the class constructor does not expect any argument at all.

For example:

import { standalone } from "wirebox";

@standalone()
class Logger {
  log(message: string) {
    console.log(message);
  }
}

The @standalone does not take any arguments.

Requires

The @requires decorator is used to define the dependencies of a class. For example the Database class requires the Logger class to work properly.

import { requires } from "wirebox";

@requires(() => [Logger])
class Database {
  #logger: Logger;

  constructor(logger: Logger) {
    this.#logger = logger;
  }

  async connect(): Promise<void> {
    this.#logger.log("Connecting to database...");

    // some logic here

    this.#logger.log("Database connected!");
  }
}

The @requires decorator takes a function which returns the list of dependencies. The reason why it uses a function is because with this approach, we eliminate circular dependencies issues and the dependency class does not need to be declared at the dependent class declaration.

By returning an empty dependency list, it will function the same way as the @standalone decorator.

Preconstruct

The @preconstruct decorator is a more advanced decorator which allows you to define some logic before the class constructor is called. This is powerful when used on an abstract class. To understand why, let's take a look at this logger example:

import { preconstruct } from "wirebox";

// some external configuration
const useConsoleLogger = true;

@preconstruct(() => useConsoleLogger ? new ConsoleLogger() : new FileLogger())
abstract class Logger {
  abstract log(message: string): void;
}

class ConsoleLogger extends Logger {
  log(message: string): void {
    console.log(message);
  }
}

class FileLogger extends Logger {
  log(message: string): void {
    // some file write logic here
  }
}

Because ConsoleLogger and FileLogger extends the Logger class (implements Logger will also work), we can use the @preconstruct decorator to create an instance of either class. So we only need to define Logger as our dependency and the preconstruct will take care of which class to instantiate.

Dependencies can be optionally defined as the second argument and will be available in the preconstruct function as first argument.

import { preconstruct } from "wirebox";

@standalone()
class Config {
  useConsoleLogger: boolean;

  constructor() {
    this.useConsoleLogger = true;
  }
}

@preconstruct(
  ([config]) => config.useConsoleLogger ? new ConsoleLogger() : new FileLogger(),
  () => [Config]
)
abstract class Logger {
  abstract log(message: string): void;
}

// ...
Preconstruct Async

The @preconstructAsync decorator is almost identical to the @preconstruct decorator, but it allows you to define async preconstruct logic, which can also be powerful in some cases. For example, you can use it on an Database class to create a database connection before the class is instantiated:

import { preconstructAsync } from "wirebox";

@preconstructAsync(async () => {
  const connection = await connectToDatabase(/* ... */);
  return () => new Database(connection);
})
class Database {
  constructor(private connection: Connection) {}

  async query(query: Query): Promise<Result> {
    // use the connection here
  }
}

And of course, the @preconstructAsync decorator can also be used with dependencies as the second argument the same way as the @preconstruct decorator.

Note: The async preconstruct function returns a Promise<() => InstanceType<T>> and not Promise<InstanceType<T>> where T is the class being preconstructed. This is because the construction of the class needs to be done synchronously inside wirebox, so we are able to attach some additional context information to the class construction. Without this, the link utility would not be working properly. But this is only necessary for the async preconstruct and not the normal preconstruct because the normal preconstruct function is already synchronous. For more information, see the link utility.

Usage without decorators

If your runtime does not support decorators or you don't want to use them for some reason, there are alternative functions which do exactly the same, but are not as convenient as decorators.

class MyClass {}

// @standalone equivalent
defineStandalone(MyClass);

// @requires equivalent
defineRequires(MyClass, () => [MyDependency]);

// @preconstruct equivalent
definePreconstruct(MyClass, ([dep1]) => new MyClass(dep1), () => [MyDependency]);

// @preconstructAsync equivalent
definePreconstructAsync(MyClass, async ([dep1]) => () => new MyClass(dep1), () => [MyDependency]);

// @postconstructAsync equivalent (combinable with any of the above)
definePostconstructAsync(MyClass, async function () { /* async setup */ });

Every decorator alternative function takes exactly the same arguments as the decorators, except for the additional first target (class) argument. The naming is the same as the decorators but prefixed with define (and camelCased).

Note: These functions should only be called once and directly after the class declaration, otherwise they may not work as expected.

Additional options

The four decorators above can only be used once per class and you can not mix them! So using two of them at the same class will not work and also makes no sense.

But there are currently two additional decorators which can be combined with any of the above decorators.

import { standalone, singleton } from "wirebox";

@standalone()
@singleton()
class Logger {
  log(message: string) {
    console.log(message);
  }
}

The @singleton decorator is used to make a class a singleton. A singleton class is a class where only one instance of this class can exist in the whole application. This is useful for classes which are expensive to create and should only be created once or classes which are unnecessary to create multiple times, for example a database connection (expensive) or a logger (unnecessary).

This decorator takes an optional Circuit as the first argument, which specifies which Circuit will be responsible to create the singleton instance. If no Circuit is specified, the default Circuit will be used (Circuit.getDefault()). Circuits will be explained in the next section.

There is also a defineSingleton function which can be used without decorators.

Postconstruct Async

The @postconstructAsync decorator allows you to run async initialization logic after a class has been constructed. This is useful when your class needs to perform async setup (e.g. loading configuration, establishing connections) that cannot happen in the constructor.

The setup argument can be:

  • A function that is called with this bound to the instance.
  • A function that returns another function, which is then called with this bound to the instance.
  • A method name (string or symbol) on the class prototype. Only methods with no parameters and a return type of void or Promise<void> are accepted.
import { standalone, postconstructAsync, tapAsync } from "wirebox";

@standalone()
@postconstructAsync("init")
class MyService {
  #config: Config | undefined;

  get config(): Config {
    if (!this.#config) throw new Error("MyService not initialized");
    return this.#config;
  }

  async init() {
    this.#config = await loadConfig();
  }
}

const service = await tapAsync(MyService); // postconstruct runs after construction
console.log(service.config); // config is loaded

Instead of a method name, you can also use the accessor pattern where the setup function returns the method to call:

@standalone()
@postconstructAsync(() => MyService.prototype.init)
class MyService {
  // ...same as above
}

Or use an inline function directly. Note that private fields (#) cannot be accessed from inline functions since they are lexically scoped to the class body — use conventional properties instead:

@standalone()
@postconstructAsync(async function () {
  // `this` is bound to the instance, but `this.#field` is not accessible here
  this.initialized = true;
})
class MyService {
  initialized = false;
}

Note: There is no synchronous postconstruct decorator — for synchronous initialization logic, simply use the class constructor. The @postconstructAsync decorator exists specifically for async operations that cannot run in the constructor.

Because @postconstructAsync involves async logic, you must use tapAsync to resolve the class. Using the synchronous tap will throw an error if the class has not been previously resolved.

There is also a definePostconstructAsync function which can be used without decorators.

2. Tapping classes

Obtaining a class instance of a wired class is called "tapping" and done via a tap function.

Instead of using the new operator, a wired class can be instantiated using the tap function.

import { tap } from "wirebox";
import { Logger } from "./logger.ts";

// Get an instance of the Logger class
const logger = tap(Logger); // The Logger class have to be wired!

console.log(logger instanceof Logger); // true

The tap function will return the instance of the class, or throw an error if the class is not wired.

Async tapping

You may remember that there is a @preconstructAsync decorator which allows you to define async preconstructors. Tapping a class with an async part (like @preconstructAsync) will not work using the synchronous tap function. Instead, you need to use the tapAsync function which returns a Promise which resolves with the instance of the class. Of course, tapAsync can also be used on classes without async parts but you have to still await them. If you don't know if the requesting class has async parts (including dependencies), you are always safe using tapAsync.

Circuits

Circuits are the part of Wirebox which are responsible for managing the instances of wired classes. Inside a circuit, there can only be one instance of a class. To explain this, let's take a look at the following example:

import { Circuit, standalone } from "wirebox";
import { Logger } from "./logger.ts";


// Create a new empty circuit (no instances inside yet)
const myCircuit = new Circuit();

// Tap the Logger class inside the newly created circuit
// This will create an instance of the Logger class and save it inside the circuit
const myLogger = myCircuit.tap(Logger);

console.log(myLogger instanceof Logger); // true

// Try to tap the Logger class with the same circuit again will result in exactly the same instance
const myOtherLogger = myCircuit.tap(Logger);

console.log(myOtherLogger instanceof Logger); // true
console.log(myLogger === myOtherLogger); // true

So, when a instance of a class is already initialized inside a circuit, tapping the class will return the same instance. If you want a new instance, you need to create a new circuit.

The default circuit

There is a default circuit which is used for singleton classes (which does not specify a different circuit, see @singleton above) and the top-level tap and tapAsync functions.

The top-level tap and tapAsync functions are just shortcuts for the default circuit:

// a simplified implementation of the "tap" and "tapAsync" functions
const tap = (target: Class) => Circuit.getDefault().tap(target);
const tapAsync = (target: Class) => Circuit.getDefault().tapAsync(target);

🔧 Advanced usage

Providers

Providers are a way to use arbitrary values as the tap result instead of the class instance. For example:

import { tap, createProvider } from "wirebox";

const HelloProvider = createProvider(() => "Hello World!");

const myValue = tap(HelloProvider); // also works as a @requires, @preconstruct, etc. dependency

console.log(myValue); // "Hello World!"

There are also specialized provider creation functions like createStaticProvider, createDynamicProvider, createAsyncProvider, and more. For details, see the documentation.

Utilities

Combine

The combine utility merges multiple classes into a single dependency that resolves to a record of their instances.

import { combine, tap } from "wirebox";

const Dependencies = combine(() => ({
  logger: Logger,
  database: Database,
}));

const deps = tap(Dependencies);
console.log(deps.logger instanceof Logger); // true
console.log(deps.database instanceof Database); // true

Lazy

The lazy utility creates a lazy-loaded class provider using dynamic imports. This is useful for code splitting.

import { lazy, tapAsync } from "wirebox";

const MyService = lazy(() => import("./my-service.ts"));

const service = await tapAsync(MyService);

By default it uses the default export, but you can specify a named export as the second argument.

With Circuit

The withCircuit utility binds a specific circuit to a class, so it always resolves from that circuit regardless of where it is tapped.

import { Circuit, withCircuit, tap } from "wirebox";

const myCircuit = new Circuit();

const BoundLogger = withCircuit(myCircuit, () => Logger);

const logger = tap(BoundLogger); // resolves Logger from myCircuit

📖 Glossary

Circuit

A circuit is a container which is responsible for managing the instances by holding and initializing them. Each circuit can only store one instance of a class. So if you want to have multiple instances of the same class, simply create a new circuit for it. You can create as many circuits as you want by simply calling new Circuit().

There is also the default circuit which can be accessed via Circuit.getDefault().

tap / tapAsync

Tapping a class simply means to resolve the class instance and return it. If the class is not yet initialized, it will be initialized and returned.

Remember for classes with async parts (like @preconstructAsync, or dependencies with async parts), you should use tapAsync.

🧪 Testing

To run tests, install the development dependencies and run the test command:

bun install
bun test

🌟 Contributing

Contributions are welcome! Please follow these steps:

  • Fork the repository.
  • Create a new branch for your feature or bugfix.
  • Submit a pull request.

We use Biome to format and lint the code, so please make sure to run bun run check before committing.

📄 License

This project is licensed under the MIT License.

Keywords

dependency injection

FAQs

Package last updated on 06 Mar 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts