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

@resynth1943/inject

v0.0.4

Published

The functional way to use Dependency Injection.

Downloads

3

Readme

@resynth1943/inject

The functional way to use Dependency Injection.

Introduction

Most Dependency Injection libraries in JavaScript are either using decorators which lose type safety, or use a Map and expect people to manually retrieve values inside class constructors. Why not do away with both of these flawed takes on how a DI system should work, and start again?

Inject takes a more functional approach to Dependency Injection. Classes are nowhere to be found.

If you're not familiar with the concept of Dependency Injection (commonly abbreviated to 'DI'), then you should read the following article as a point of reference:

A quick intro to Dependency Injection: what it is, and when to use it ─ Bhavya Karia

Example

You can view the source tree of the example here

Here's a quick example of how Inject works:

Your first Service

So let's start out with a simple service that logs messages to the console. We'll call it the LoggerService, as the recommended way to name services is *Service.

import { useKey, createService } from '@resynth1943/inject';
import { $Console } from './example';

function log (message: string) {
    const console = useKey($Console);
    console.log(message);
}

export const LoggerService = createService({
    log
});

Putting it all together

Pretty simple, huh? Now let's create something that uses this service as a dependency.

import { Domain, runInDomain, createKey, useKey, provide, useService } from '@resynth1943/inject';
import { LoggerService } from './logger';

export const $Console = createKey<Console>('console');
export const $Message = createKey<string>('message');

const appDomain: Domain = {
    providers: [
        provide($Console, console),
        provide($Message, 'Hello, world!')
    ]
};

runInDomain(appDomain, () => {
    const logger = useService(LoggerService);
    logger.log('hello, world!');

    const message = useKey($Message);

    if (typeof document === 'object') {
        document.body.innerHTML = message;
    } else {
        logger.log(message);
    }
});

So we've just done a few things here:

  1. Assigned some keys to name DI values.
  2. Created a domain to run this application.
  3. Grabbed the LoggerService from inside the domain.
  4. Used the service to do some fancy logging.

As you can see, Inject is actually pretty simple to use. Read on for more information about the above example, and how it works.

Glossary

  • key: The name of a dependency. This is used to get the value of a dependency from inside a domain.
  • domain: A descriptor providing instructions for how DI should work from inside a specialized execution context (a callback).
  • provider: An object that provides a value for a dependency. This is then retrieved using a key.
  • service: A container holding functions and state relevant to a specific task.

As you can see, there are two keys on every provider object: key; provide. The key key describes the key for which we are providing a value. The provide key describes the value we are providing.

Getting Started

If you want to get started quickly, I've created an example project which can be found here. You can play with this example project on CodeSandbox, if you prefer an online environment. Otherwise, execute the following commands in your shell:

$ git clone https://github.com/resynth1943/inject
$ tsc
$ node ./lib/example/example.js

Keys

In Inject, all DI values are labeled with keys. To create a key, use the following syntax:

import { createKey } from '@resynth1943/inject';

export const $Key = createKey<KeyType>('KeyDescription');

Providers

The concept of providers is crucial to Inject. A provider provides a value for a key. A provider looks like the following:

interface Provider<TKey extends Key<unknown> = Key<unknown>> {
    key: TKey;
    provide: GetKeyType<TKey>;
}

This is then passed to the providers field of the domain. When requesting the value of a key from inside the domain, the appropriate value will be yielded.

So we've just created a key that's equivalent to Symbol(DI.Key.KeyDescription) (but that's an implementation detail, don't worry too much about that).

You can then use this key in a provider map to provide a basic value.

Default values

When looking up a key, you can also provide an optional default value. This allows you to call upon a dependency that has not been explicitly declared.

Take the following example:

import { useKey } from '@resynth1943/inject';

runInDomain(domain, () => {
    const value = useKey($Key, 'your default value');
});

If $Key has not been provided by the domain, the useKey function will return 'your default value'.

Bear in mind that the default value must be the same type as the value of the Key.

Domains

Domains are crucial to Inject. Calling useKey outside of the execution context of a domain is not allowed.

A domain is essentially a descriptor for a Dependency Injection execution context. Take the following code:

import { Domain, provide } from '@resynth1943/inject';
import { $Key } from './shared/keys';

const domain: Domain = {
    providers: [
        provide($Key, 2)
    ]
}

To retrieve the value of a key inside this execution context, you simply do the following:

import { runInDomain, useKey } from '@resynth1943/inject';
import { $Key } from './shared/keys';

runInDomain(domain, () => {
    const value = useKey($Key);
    // use `value` here!
});

Declaring values

Declaring values allows you to provide a value for a Key.

To declare a value for a key when called inside a domain, simply do the following:

import { runInDomain, Domain, provide, createKey, useKey } from '@resynth1943/inject';

const $OurNumber = createKey<number>('OurNumber');

const domain: Domain = {
    providers: [
        provide($OurNumber, 2)
    ]
}

runInDomain(domain, () => {
    const value = useKey($OurNumber);
    // The above will yield 2.
});

Services

Think of a service as a manager for a specific task. Don't place too much logic in one service.

A service is a module, containing necessary functions and state to run isolated tasks.

Any object is a valid service. You can create a service like so:

import { createService } from '@resynth1943/inject';

export const LoggerService = createService({
    log,
    error,
    warn
});

As a general rule of practice, you should avoid exporting the properties of your service (log, error and warn in this example) outside of the service.

You can acquire a service like so:

import { LoggerService } from './services/logger';
const logger = useService(LoggerService);

In most circumstances, useService will be an identity function. If the domain overrides this service though, the override will be returned in place of the first argument.

Defining a Service

You define a required service in your domain, like so:

import { Domain } from '@resynth1943/inject';
const domain: Domain = {
    services: [LoggerService]
}

Internally, Inject binds all Services to Keys, and calls upon those keys to find the service in the domain registry.

Extending a Service

You can also use provideService to extend a service, like so:

import { Domain, provideService } from '@resynth1943/inject';
const domain: Domain = {
    services: [provideService(LoggerService, CoolLoggerService)]
}

When useService is called with LoggerService, CoolLoggerService will be returned. This is classical Dependency Injection, allowing you to provide stub versions of dependencies in testing.

License

This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.