npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@baileyherbert/container

v3.0.0

Published

Lightweight dependency injection for TypeScript.

Downloads

68

Readme

Container

This package contains a simple, lightweight dependency injection container. It's heavily inspired by tsyringe but has some cool additional features and uses my reflection library.

  • Transient & singleton resolution
  • Child containers & container-scoped singleton resolution
  • Decorators for singleton and transient registration
  • Easily invoke methods with dependency injection
  • Named global containers
  • Easy container resolution within dependencies at constructor time

Installation

npm install @baileyherbert/container

Make sure your tsconfig.json file specifies the following options:

{
    "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
    }
}

Documentation

Global container

Import the global container from anywhere:

import { container } from '@baileyherbert/common';

Registration

Then register your types using injection tokens using the register methods.

container.register(ClassType);
container.register(ClassType, { useClass: ClassType });
container.register(ClassType, { useValue: new ClassType() });
container.register(ClassType, { useFactory: () => new ClassType() });

container.registerSingleton(ClassType);
container.registerSingleton(ClassType, ClassType);

container.registerInstance(new ClassType());
container.registerInstance(ClassType, new ClassType());

When registering a class or token provider, or a type, you can provide a lifecycle:

container.register(ClassType, { lifecycle: Lifecycle.Singleton });
container.register(ClassType, { useClass: ClassType }, { lifecycle: Lifecycle.ContainerScoped });
  • Transient creates a new instance for each resolution. This is the default.
  • Singleton creates a single instance and caches it for subsequent resolutions.
  • ContainerScoped creates a single instance per container (i.e. child containers will get their own).

Decorators

For the container to successfully resolve dependencies, all classes added to it must have the @Injectable decorator applied.

@Injectable()
export class ClassType {}

You can also register a class as a singleton on the global container using the @Singleton decorator. This will also mark the class as injectable so there's no need to add the @Injectable decorator.

@Singleton()
export class ClassType {}

You can also enable dependency injection on a class method by applying the @Injectable decorator to it.

@Singleton()
export class ClassType {
    @Injectable()
    public methodWithDI() {

    }
}

There is a @Transient decorator that works just like @Singleton but registers the class as transient instead.

@Transient()
export class ClassType {

}

Finally the @Token decorator can be applied to method parameters in order to specify or override the injection token. This is especially useful when dealing with circular dependencies.

public method(@Token('tokenName') param: any) {}
public method(@Token(Class) param: any) {}
public method(@Token(() => Class) param: any) {}

Resolution

To resolve a single instance, use the resolve method. The last provider to be registered will be used.

const instance = container.resolve(ClassType);

If multiple providers are registered, you can retrieve all of their instances as an array with the resolveAll method.

const instances = container.resolveAll(ClassType);

Child containers

You can create child containers on demand. By registering a dependency on a child container, you can override the return value of the resolve method. The resolveAll method will return an array of dependencies from both containers in the order of registration, and with the child container's dependencies last.

const child = container.createChildContainer();
child.registerInstance(ClassType, new ClassType());

Dispatchers

To invoke methods with dependency injection, first create a dispatcher.

const dispatcher = container.createDispatcher();

You can add custom typed instances which override the container. You can also add named values. If the method has a parameter which fails to resolve with the container or has a primitive type, but has a matching named value, then the named value will be used.

dispatcher.setNamedParameter('name', 'John Doe');
dispatcher.setTypedParameter(ClassType, new ClassType());

Finally, use the invoke method to resolve dependencies, execute, and get the return value.

const returnValue = dispatcher.invoke(object, 'methodName');

resolver

This helper manages global container instances and makes it easy for various parts of the application to retrieve a reference to specific containers.

Named containers

If the global container is not sufficient, you can use named containers. Simply request a named container and it will be created and cached globally.

import { resolver } from '@baileyherbert/common';

const container = resolver.getInstance('name');

Container references

If your application is using multiple containers, you might be interested in storing a reference to the container used to construct an object. Generally, this would require injecting the container as a parameter.

The resolver instead makes the container available with the getConstructorInstance() method, but note that this method will throw an error if not called from within a constructor that has been invoked by the container during DI.

Here's a reliable pattern for storing the container that works even if the class is extended:

import { resolver } from '@baileyherbert/common';

export class DependencyInjectedClass {
    protected container = resolver.getConstructorInstance();

    public constructor() {
        // Now all methods, including the constructor, has a reference to the container
        this.container.resolve();
    }
}

With a reference to the container, you could make it easier for nested components in your application to retrieve top level objects, like a root App object.

export class DependencyInjectedClass {
    protected container = resolver.getConstructorInstance();
    protected app = this.container.resolve(App);
}

Context

When registering multiple values under a single token, context allows you to pick a specific instance out of the registry.

Introduction

Context is useful because it allows you to do things like this:

class Service {
    constructor(
        @Context('db:one') db1: Database,
        @Context('db:two') db2: Database
    ) {}
}

In the above example, we have two different Database instances which have been registered with their database names as context, allowing us to easily pick out the instance(s) we need while still using the Database token.

You can use string tokens for the same effect, but this permits some organization and works nicely with the resolveAll() method.

Registration

When registering an instance or value, you can pass a context value of any type. If there is already a contextual value under the same token with the same name, it will be overwritten.

Please note that when supplying an array for your context, it will be destructured into multiple contexts. This only happens for arrays, not any other types (such as sets).

container.registerInstance(new Class(), 'context');
container.register(Class, {
    useValue: new Class(),
    useContext: 'context'
});

Resolution

You can resolve a value from its context by passing the context into the resolve() method.

container.resolve(Class, 'context');

You can also use the @Context() decorator in a class or method parameter.

constructor(@Context('context') instance: Class) {}

You can also pass an array of contexts to match against. The container will attempt to resolve a value for each context in the order specified, until a match is found and returned.

container.resolve(Class, ['contextA', 'contextB']);