@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:
- Assigned some keys to name DI values.
- Created a domain to run this application.
- Grabbed the
LoggerService
from inside the domain. - 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 Service
s to Key
s, 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/.