TypeScript opinionated dependency injection library. Glue is a tiny lightweight library made for FP codebases making extensive use of partial application for dependency injection.
npm install ts-glueTypeScript opinionated dependency injection library.
Why another dependency injection library?
Most dependency injection libraries target object oriented codebases.
Glue (\[/ɡluː/\], should you need to say it out loud) is a tiny lightweight library made for FP codebases making extensive use of partial application for dependency injection.
Main benefits of Glue include:
- TypeScript type level configuration checks: Glue allows to check at buidtime that all dependencies have been registered.
- Lazyness: A function or a component managed by Glue can be used anywhere in your codebase without worrying when and how dependencies will be set up at runtime.
- Modularity: Glue is monorepo friendly, configuration can be splitted into several modules without losing buildtime checking nor lazyness.
If you are in a hurry you can start playing with ts-glue right away with this demo sandbox
ts-glue is a TypeScript library that can be installed with any package manager such as npm or yarn:
``sh
# with npm
npm i ts-glue
# or yarn
yarn add ts-glue
`
In order to use ts-glue, you need to do 3 things:
- Build a Glue object that will hold a descriptions of all the components and functions that might need to be injected
- Register implementations
- Use the glue to inject the implementations previously registered
So let's begin by building a "Glue" object and a description of all the functions and components that it will handle.
Let's say we have a clock function we want to inject in our codebase:
`typescript
type Clock = () => Date;
const systemClock: Clock = () => new Date();
`
Our _Glue_ could be set up as shown below:
`typescript
import { Glue, is } from "ts-glue";
const glue = Glue.buildFrom({
clock: is
}).registerService("clock", systemClock);
`
Our glue is now ready for use!
It can be used like a regular glue:
`typescript`
const clock: Clock = glue.getService("clock");
But the ts-glue sweet spot comes with functions that can be partially applied as shown below:
`typescriptHello world ${name} (${clock()})
const doHelloWorld = (clock: Clock) => (name: string) =>
;
const helloWorld = glue.inject(doHelloWorld, ["clock"]);
helloWorld("Glue");
`
Our _helloWorld()_ is now ready for use. If we want to inject dependencies into an object instead of a function, we need to use _Glue.build()_ instead of _Glue.inject()_:
`typescriptHello world ${name} (${clock()})
const buildHelloWorld = (clock: Clock) => ({
sayHello: (name: string) => ;
}):
const helloWorld = glue.build(buildHelloWorld, ['clock']);
helloWorld.sayHello('Glue');
`
Glue functions such as _registerService()_ or _inject()_. This means that if you do a typo, TypeScript will yell at you!
`typescript
import { Glue, is } from "ts-glue";
const glue =
Glue.buildFrom(
{
clock: is
}
)
const doHelloWorld = (clock: Clock) => (name: string) => Hello world ${name} (${clock()});
glue.registerService('cloq', systemClock); // Compilation error
glue.registerService('clock', () => 'string')); // Compilation error
glue.inject(doHelloWorld, ['cloq']); // compilation error
glue.inject(doHelloWorld, []); // compilation error
`
You can also ask ts-glue to check that your configuration is complete:
`typescript
import { Glue, is } from "ts-glue";
const glue = Glue.buildFrom({
clock: is
dbConfiguration: is
}).registerService("clock", systemClock);
// compilation error, dbConfiguration is missing
glue.checkAllServicesAreRegistered();
const glue2 = glue.registerService("dbConfiguration", someDbConfiguration);
// compilation OK
glue2.checkAllServicesAreRegistered();
`
Feel free to experiment with ts-glue in this playground
ts-glue is very lazy :-)
Function dependencies are resolved at the very last moment, which is when they get executed. This means that you do
not have to worry too much ot the sequence order of injections and registrations:
`typescript
import { Glue, is } from "ts-glue";
type Random = () => number;
const doGiveMeANumber = (random: Random) => A random number ${random()};
const glue = Glue.buildFrom({
randomGenerator: is
}).registerService("randomGenerator", Math.random);
const giveMeANumber = glue.inject(doGiveMeANumber, ["randomGenerator"]);
glue.registerService("randomGenerator", () => 42);
giveMeANumber(); // A random number 42
`
In the example above, we first build a glue, we register a first random number generator and we retrieve an injected version of the
_giveMeANumber()_ function. Then we override the registered random number generator. Since dependency injection is lazy, since dependencies are resolved each time an injected function get executed, _giveMeANumber()_ calls the very last registered random generator.
ts-glue lazyness is very handy when one part of your codebase is managed by ts-glue but not everything.ts-glue
We have included in the example folder an Express express example app that demonstrate how to use components managed by from Express routes that are out of the scope of ts-glue.
Any significant codebase is splitted into several modules, packages... well it should be ;)
A big monolythic dependency injection configuration file quickly becomes hard to maintain. Hence ts-glue allows you to split your configuration in several files, composing your _Glue_ from several sub _Glue_ instances:
`typescript
// Let's say we have a booking package in our codebase
// booking.ts
export const bookingGlue =
Glue.buildFrom(
{
bookingService: is
dbConfiguration: is
...
}
).registerService(
'bookingService',
someBookingServiceImplementation
);
// Let's say we have also a billing package in our codebase
// billing.ts
export const billingGlue =
Glue.buildFrom(
{
billingService: is
dbConfiguration: is
...
}
).registerService(
'billingService',
someBillingServiceImplementation
);
// Then at the entry point of our application
// we can gather our two previous glues
const appGlue = Glue.compose(
bookingGlue,
billingGlue
);
// appGlue can inject 'bookingService' and 'billingService'
const megaSagaService =
appGlue.inject(
...,
['bookingService', 'billingService']
)
// at that point line below fails to compile because
// dbConfiguration has not been registered
appGlue.checkAllServicesAreRegistered();
// Now it's ok and the dbConfiguration is registered
// into both bookingGlue and billingGlue
appGlue
.registerService('dbConfiguration', SomeDbConfig)
.checkAllServicesAreRegistered();
``
See for yourself how ts-glue leverages on TypeScript type checking with this playground
TBC