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

checked-inject

v0.1.18

Published

[Documentation](https://youngspe.github.io/checked-inject/)

Downloads

32

Readme

checked-inject

Documentation

npm install --save checked-inject

checked-inject is a TypeScript dependency injection library that verifies all dependencies are met at compile time.

class NameKey extends TypeKey<string>() { private _: any }
class IdKey extends TypeKey<number>() { private _: any }

class User {
  name: string
  id: number
  constructor(name: string, id: number) {
    this.name = name; this.id = id
  }

  static inject = Inject.construct(this, NameKey, IdKey)
}

class App {
  user: User
  constructor(user: User) {
    this.user = user
  }
}

const UserModule = Module(ct => ct
  .provideInstance(NameKey, 'Alice')
  .provideInstance(IdKey, 123)
)

const AppModule = Module(UserModule, ct => ct
  .provide(App, { user: User }, ({ user }) => new App(user))
)

AppModule.inject({ app: App }, ({ app }) => {
  console.log(`Welcome, ${app.user.name}`)
})

Injection

Containers

A Container is used to provide injections and request resources, solving the dependency graph at compile time and runtime. An empty container is obtained through Container.create()

Providing Resources

You can build a container by creating it, then providing resources in a method chain. It's important to use the last container returned from the method chain so the type system is aware of the full dependency graph.

In the following methods, key is either a TypeKey or a class.

provideInstance(key, value) specifies a value to directly return when the given key is requested.

provide(key, [scope], dep, init) supplies a function that will be called when the given key is requested. dep specifies the dependencies of the resource you're providing. The dependencies are passed to the function init, which returns the resources' value. scope is an optional parameter. It can either be a Scope or an arbitrarily-nested list of Scopes. When provided, the provided resource is bound to the given Scope(s).

provider(key, [scope], computedKey) supplies a ComputedKey that resolves to the resource to be returns when key is requested.

const container = Container.create()
  .provideInstance(NameKey, 'Alice')
  .provideInstance(IdKey, 123)
  .provide(User,
    { name: NameKey, id: IdKey }, ({ name, id }) => new User(name, id))
  .provide(Foo, Inject.map(IdKey, id => new Foo(id)))

Requesting Resources

// 'request' returns the resource identified by the given DependencyKey
let user: User = container.request(User)
// 'inject' calls the given function with the resource as an argument
container.inject(User, user => console.log(user.name, user.id))

Compile-Time Checks

This library is centered around verifying all dependencies are met within the type system.

If you request a resource and any of its transitive dependencies are unavailable, there will be a compile error ending with something like: Type 'void' is not assignable to type 'typeof Foo | typeof Bar'. where Foo and Bar are resources that are needed but not provided.

Scopes, Child Containers, and Subcomponents

A child container is a container that inherits all of its parent's provided resources. It can be created with Container#createChild.

A container can be assigned one or more Scopes, and a provided resource can be associated with one or more Scopes. When a resource is scoped, its value is stored in the nearest container in the ancestor chain that either:

  • Has a provider for the resource
  • Contains one or more of the requested scopes

Because of this rule, if you provide a scoped resource to a child container of the one assigned the scope, the stored value will not be visible to consumers of the parent container or its sibling containers, and it will be safe to depend on resources also provided to the child container:

In the following example, it's safe for the 'Baz' to reference 'Bar' even though 'Bar' isn't in the parent container or bound to 'MyScope because it's provided in the child container rather than the parent:

const parent = Container.create()
  .addScope(MyScope)
  .provide(Foo, {}, () => new Foo())

const child = parent.createChild()
  .provide(Bar, { foo: Foo }, ({ foo }) => new Bar(foo))
  .provide(Baz, MyScope, { bar: Bar }, ({ bar }) => new Baz(bar))

const baz1 = child.request(Baz)
const baz2 = child.request(Baz)

console.log(baz1 === baz2) // prints 'true'

This example will fail to compile with: Type 'void' is not assignable to type 'typeof Bar'. because Bar is not found in MyScope from the container where Baz is provided:

const parent = Container.create()
  .addScope(MyScope)
  .provide(Foo, {}, () => new Foo())
  .provide(Baz, MyScope, { bar: Bar }, ({ bar }) => new Baz(bar))

const child = parent.createChild()
  .provide(Bar, { foo: Foo }, ({ foo }) => new Bar(foo))

const baz = child.request(Baz)

If you request a resource and any Scope it's bound to is not assigned to this container or its ancestors, there will be a compile error ending with something like: Type 'void' is not assignable to type 'typeof MyScope'. where MyScope is a Scope a dependency is bound to but is not available.

Singleton

Singleton is a built-in scope that's automatically assigned to a new Container. Use Singleton when you only want to create one instance of a resource. If you want an instance to have a shorter lifetime, you can create a custom Scope.

Custom Scopes

To ensure each Scope has its own distinct type, a Scope is declared as a class extending Scope() with at least one private member:

class MyScope extends Scope() { private _: any }

Subcomponents

A Subcomponent transforms a list of arguments into a new child container.

The Inject.subcomponent method takes a lambda that initializes a child container given a list of arguments. It returns a ComputedKey that resolves to a Subcomponent.

class UserScope extends Scope() { private _: any }
const UserComponent = Inject.subcomponent((ct, name: string, id: number ) => ct
  // UserScope allows us to cache dependencies that are only valid when a name
  // and id are available
  .addScope(UserScope)
  .provideInstance(NameKey, name)
  .provideInstance(IdKey, id)
)

const parent = Container.create()
  // Binding the 'User' resource to 'UserScope' allows us to cache a single
  // intance and return the same one every time 'User' is requested.
  // 'Singleton' would be insufficient because 'NameKey' and 'IdKey' are
  // provided in a child container, so they are unavailable in the Singleton
  // scope.
  .provide(User, UserScope, Inject.construct(NameKey, IdKey))

// This will cause a compile error because `UserScope' is unavailable:
// const user1 = parent.request(User)

// The following three lines are equivalent:
const child1 = parent.request(UserComponent)('Alice', 123)
const child2 = parent.request(UserComponent.Build('Alice', 123))
const child3 = parent.build(UserComponent, 'Alice', 123)

// This is okay because child1 is in 'UserScope', and both 'NameKey' and 'IdKey'
// are provided:
const user2 = child1.request(User)

Modules

You can group providers into Modules using the Module() function. This function takes a lambda that accepts a Container.Builder to which you can provide resources just like a Container. Container#apply(module) applies all providers in the Module to the container. Modules also have a container() method that returns a new container with the Module applied to it. Additionally, Modules have request[Async] and inject[Async] methods that internally creates a Container and calls the respective Container method.

const UserModule = Module(ct => ct
  .provideInstance(NameKey, 'Alice')
  .provideInstance(IdKey, 123)
  .provide(User, Inject.construct(User, NameKey, IdKey))
)

// These two lines are equivalent:
const container1 = Container.create().apply(UserModule)
const container2 = UserModule.container()

// You also call 'request[Async]' or 'inject[Async]' without directly creating
// a 'Container':
UserModule.inject(User, user => {
  console.log(user.name, user.id)
})

Modules can be combined into a single module:

const AppModule = Module(UserModule, DataModule, FooModule)

AppModule.inject(App, app => {
  // ...
})

Asynchronous Resources

Not all resources are available immediately. Using Container#provideAsync, you can provide a Promise that will eventually resolve to the provided resource. Any resource depending on an asynchronous resource is itself resolved asynchronously.

Asynchronous resources can be resolved with Container#requestAsync or Container#injectAsync. These methods are analogous to their non-async counterparts. The only differences are:

  • The async versions can resolve asynchronous resources
  • They return a Promise rather than directly returning the value
  • injectAsync's function can be an async function
let container = Container.create()
  .provideInstance(NameKey, 'Alice')
  .provideAsync(IdKey, {}, async () => 123)
  .provide(User, Inject.construct(User, NameKey, IdKey))

// This is okay since 'NameKey' is available synchronously:
let name1: string = container.request(NameKey)

// This will fail since 'IdKey' is an asynchronous resource:
// let id1: number = container.request(IdKey)

// This will fail since 'User' depends on 'IdKey', which is asynchronous:
// let user1: User = container.request(User)

// The following are all allowed:
let name2: Promise<string> = container.requestAsync(NameKey)
let id2: Promise<number> = container.requestAsync(IdKey)
let user2: Promise<User> = container.requestAsync(User)

To synchronously resolve a Promise for the resource rather than asynchronously waiting for the resource to complete, use Inject.async or (TypeKey|ComputedKey|typeof Injectable)#Async():

let user1: Promise<User> = container.request(Inject.async(User))
// This works if 'User' extends 'Injectable':
let user2: Promise<User> = container.request(User.Async())

If you request an asynchronous resource synchronously, there will be a compile error ending with something like: Type 'void' is not assignable to type 'NotSync<typeof Foo>'. where Foo is an asynchronous resource.

DependencyKey

A DependencyKey identifies a resource that can be injected. It may be a dependency of another resource or requested directly from a container.

There are multiple kinds of DependencyKeys:

Class

An InjectableClass<T> is any class object that optionally has a static scope: extends ScopeList and/or a static inject: extends ComputedKey <T> property.

Examples

class User {
  name: string
  id: number
  constructor(name: string, id: number) {
    this.name = name; this.id = id
  }
}
class UserManager {
  private userApiClient: UserApiClient
  constructor(userApiClient: UserApiClient) {
    this.userApiClient
  }
  // If 'UserManager' is not explicitly provided to the container, the value of
  // 'inject' will be used to resolve it instead.
  static inject = Inject.construct(this, UserApiClient)
}
abstract class UserApiClient {
  abstract getUser(id: number): Promise<User>

  // Instances of 'UserApiClient' will be stored and reused for all containers
  // marked with the scope called 'UserScope'
  static scope: UserScope
}
class ApiConfig {
  appId: string
  apiKey: string

  // All root containers are automatically marked with 'Singleton', so a single
  // instance of 'ApiConfig' will be created and reused within the container
  // it's provided to.
  static scope: Singleton
}

Injectable

Though optional, classes can extend Injectable, which does the following:

  • Prevent static scope from being assigned a value that does not extend ScopeList
  • Prevent static inject from being assigned a value that does not extend ComputedKey <T>
  • Add static operator methods equivalent to those on TypeKey and ComputedKey and analogous to the methods on Inject like Lazy(), Provider(), Async(), Optional()
class User extends Injectable {
  name: string
  id: number
  constructor(name: string, id: number) {
    super(); this.name = name; this.id = id
  }
}

// Later, we can use operators like 'Optional()' when requesting 'User':

const MyModule = Module(ct => ct
  .provide(Foo, { user: User.Optional() }, ({ user }) => new Foo(user))
)

TypeKey

A TypeKey specifies a resource not tied to a specific class object--like a named dependency.

To ensure each TypeKey has its own distinct type, a TypeKey<T> is declared as a class extending TypeKey<T>() with at least one private member:

class NameKey extends TypeKey<string>() { private _: any }
class IdKey extends TypeKey<number>() { private _: any }

// You can set a 'ComputedKey' like `Inject.map(...)' as a default provider.
// If 'CurrentUser' is not explicitly provided to a container, the default
// provider will be used to resolve it.
class CurrentUserKey extends TypeKey({
  default: Inject.map([NameKey, IdKey], ([name, id]) => new User(name, id)),
}) { private _: any }

A TypeKey may also have a static scope: extends ScopeList that binds its resource to one or more scopes:

class MyKey extends TypeKey<string>() {
  private _: any
  static scope = Singleton
}

Structured Keys:

DependencyKeys can also be structured into arrays or objects of the form DependencyKey[] or { [k: string]: DependencyKey } like so:

const [name, id, user] = container.request([NameKey, IdKey.Provider(), User])
const { name, id, user } = container.request({
  name: NameKey,
  id: IdKey.Provider(),
  user: User,
})

If you want you can even nest structured keys:

const { userInfo: { name, id } }  = container.request({
  userInfo: { name: NameKey, id: IdKey },
})

ComputedKey

A ComputedKey transforms a dependency into another type. Implementations are found in the Inject namespace.

These methods can operate over any DependencyKey, even structured keys:

const f = container.request(Inject.provider({
  name: NameKey,
  id: IdKey,
}))

const { name, id } = f()

Some common ComputedKeys include the following:

Lazy, Provider

Inject.lazy(src) and Inject.provider(src) both resolve to a function returning the target type of src. lazy caches the result after the first call, whereas provider resolves the resource every time it's called. TypeKeys, ComputedKeys, and class objects that extends Injectable have analogous methods called Lazy() and Provider().

Because the resulting function returns synchronously, these methods can only be used on dependencies that can be resolved synchronously. To resolve asynchronously, use Inject.async(MyKey).Lazy()/Provider() or (if MyKey is a TypeKey or class object that extends Injectable) MyKey.Async().Lazy()/Provider().

This will yield a value of type () => Promise<Target<MyKey>>.

Async

Inject.async(src) resolves the given dependency as a Promise. This allows you to request dependencies that cannot be resolved synchronously without having to use requestAsync or injectAsync. Instead the dependent resource can await the promises independently.

See also Async()

class UserInfo {
  name: string
  private _id: Promise<number>
  getIdAsync() { return _id }

  constructor(name: string, id: Promise<number>) {
    this.name = name; this._id = id
  }
}

const ct = Container.create()
//.provide(UserService, ...)
  .provideAsync(IdKey,
    { service: UserService}, ({ service }) => service.getIdAsync())
  .provide(UserInfo, Inject.construct(UserInfo, NameKey, IdKey.Async()))

// This would be a compile error because IdKey is not provided synchronously:
// const id = container.request(IdKey)

// This is okay because Async() provides the value synchronously as a promise:
let id = await container.request(IdKey.Async())
// It's equivalent to this:
id = await container.requestAsync()

// This is okay even though it requires IdKey because it uses Async():
const userInfo = container.request(UserInfo)

// userInfo is created but we still have to await IdKey's promise to get the id:
id = await userInfo.getIdAsync()

Map

Inject.map(dep, transform) resolves to a transformation over another dependency.

See also Map(transform).

const user: User = container.request(Inject.map({
  name: NameKey,
  id: IdKey,
}), ({ name, id}) => new User(name, id))

This is useful for default injections:

class User {
  name: string
  id: number
  constructor(name: string, id: number) {
    this.name = name; this.id = id
  }

  static inject = Inject.map({
    name: NameKey,
    id: IdKey,
  }, ({ name, id }) => new User(name, id))
}

class UserKey extends TypeKey({
  default: Inject.map({
    name: NameKey,
    id: IdKey,
  }, ({ name, id }) => new User(name, id))
}) { private _: any }

Construct

Inject.construct(ctor, ...deps) resolves to the instantiation of the given class contructor given the following dependencies. It's equivalent to Inject.map(ctor, [...deps], ([...args]) => new ctor(...args)).

const user: User = container.request(Inject.construct(User, NameKey, IdKey))

This is useful for default injections:

class User {
  name: string
  id: number
  constructor(name: string, id: number) {
    this.name = name; this.id = id
  }

  static inject = Inject.construct(this, NameKey, IdKey)
}

class UserKey extends TypeKey({
  default: Inject.construct(User, NameKey, IdKey)
}) { private _: any }

or dependency providers:

const container = Container.create()
  .provide(User, Inject.construct(User, NameKey, IdKey))

Target Types

Target<K>, where K extends DependencyKey, indicates what type the K resolves to when requested from a container:

TypeKey <string>

TypeKey <number>

InjectableClass <User>

ComputedKey

One of:

NameKey.Provider()
Inject.provider(NameKey)

() => string

ComputedKey

One of:

IdKey.Map(id => id.toString())
Inject.map(IdKey, id => id.toString())

string

ComputedKey

One of:

Inject.async(User).Lazy()
Inject.lazy(Inject.async(User))
// If 'User' extends 'Injectable':
User.Async().Lazy()

() => Promise<User>

{
  name: NameKey,
  id: IdKey.Provider(),
  user: User,
}
{
  name: string,
  id: () => number,
  user: User,
}
[NameKey, IdKey.Provider(), User]
[string, () => number, User]