Needle
Seamless dependency injection for Typescript
Installation
From npm:
npm i needle-inject
Usage
Important: When using the decorator, make sure you have the flags emitDecoratorMetadata
and experimentalDecorators
both set to true in tsconfig.json
{
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
Any class can be used as a singleton using needle, no extra decorations allowing for backwards compatiblity.
Examples
Usage should be pretty straightforward, we have a service class we want to inject into another class/function. To achieve this we do not have to touch the original class simply make sure it has a constructor that takes no arguments.
class FooService {
procedure(obj : number) : boolean {
// do complex logic
return obj % 2 != 0
}
requestThing() : Promise<any> {
// Send async call
}
}
To use FooService:
-
Using function injection:
import { provide } from "needle-inject" import FooService from "./FooService" const service = provide(FooService) console.log(service.procedure(2))
-
With class injection:
import { inject } from "needle-inject" import FooService from "./FooService" class Goopus { @inject private fooService : FooService; constructor(obj : number) { console.log(fooService.procedure(obj)) } } let goopus = new Goopus(3) console.log(goopus.fooService.procedure(6))
-
Using auto injection:
import { autoInject, create } from "needle-inject" import FooService from "./FooService" @autoInject class Bloopus { constructor(private fooService : FooService) { } evaluate(obj : number) { console.log(this.fooService.procedure(obj)) } } let bloopus = create(Bloopus); bloopus.evaluate(13)
These run in succesion will produce:
false
true
false
true
With React
Here is an example component that uses FooService
:
import * as React from "react";
import { inject } from "needle-inject"
import FooService from "./FooService"
export default class MyComponent extends React.Component {
@inject
private fooService : FooService;
render() {
return <div>
<Button onClick={this.fooService.requestThing()}>Send Request</Button>
<p>
Procedure of 3 is {this.fooService.procedure(3)}
</p>
</div>
}
}
Use case with React & MobX
You can run an example app located in examples/react-mobx
. Run npm start
and navigate to localhost:1234 to view it.
When managing state in React.js a lot of the time you want a reference to a global store. When using the very useful library MobX they provide a very unopinionated way to handle the passing of stateful objects. Here is where needle becomes useful!
-
Create a store class that will store state:
import { observable, computed } from "mobx" // I store the state for auth sessions class AuthStore { @observable userToken : String | null; @observable userSession : any; @computed get isLoggedIn() { return this.userToken != null; } }
-
Create a service that will hold this store:
import { action } from "mobx"; export default class AuthService { store: AuthStore; constructor() { store = new AuthStore(); } @action logIn() { // Do complex logic here... this.store.userToken = "TOKEN_FROM_AUTH"; } }
-
Now we can inject AuthService, observe that store and any changes to that state will update our components:
import * as React from "react"; import { observer } from "mobx-react"; import { inject } from "needle-inject"; import AuthService from "./AuthService"; @observe export default class MyComponent extends React.Component { @inject private authService : AuthService; render() { return this.authService.isLoggedIn ? <div>I am logged in!</div> : <a href="/login">Login?</a>; } }
Usage with Babel 7
Babel 7 includes it's own Typescript parser but unfortunately, from what I could find, it does not support emitting metadata that is required for the @inject annotation to function.
My suggested workaround is to pre-transpile using tsc (ts-loader with webpack etc.) and then feeding the result into Babel 7 until they support this feature.
Next.js and Babel 7
To mitigate the fact that next.js uses Babel 7 there is an addon for next.config.js
bundled. To use needle & typescript with next.js:
const addons = require("needle-inject/dist/addons");
const {withNeedle} = addons.next;
module.exports = withNeedle({})
How it works
There isn't actually much going on under the hood. We keep a singleton InjectionManager
which stores all the current mappings. When a class instance is requested we check if we already have one and serve it, otherwise creating a new instance and storing it for next time.
Limitations
-
I've yet to explore what happens when multiple libraries use needle at the same time. The problem I see arisng is two libraries using two impls of the same base class. Let's say they share a common lib with
AuthService
as an abstract base class yet they both use seperate services that inherit from this. One will clash with the other. -
More complicated functionality has not yet being needed as part of my requirements but when they arise I fear they will complicate the current minimalist impl. Overthinking it? Probably.