Navigation

Dependency injection

What is a dependency#

We’ve talked about how new LazyPromise(foo) is really just a wrapper around foo. Dependency injection is about being less restrictive about what kind of functions LazyPromise can wrap: namely, in addition to the first parameter of the shape { resolve, reject }, we also allow a second parameter called “dependency” that can be of any type:

const lazyPromise = new LazyPromise<MyValue, MyDep>(
  (
    sink,
    dep, // Type is `MyDep`.
  ) => ...,
);

lazyPromise.subscribe(
  consumer,
  dep, // Must satisfy `MyDep`.
);

The dependency type parameter is optional. If you omit it, it will default to unknown, indicating that there are no dependencies.

Dependencies bubble up through the type system when you use the operators or the generator syntax, so for example if promiseA has dependency A and promiseB has dependency B, all([promiseA, promiseB]) will have dependency A & B, in other words all needs a dependency that it’ll be able to pass to both promiseA and promiseB. This is useful for testing since you can gather up a bunch of dependencies needed by your async logic, and then satisfy them with either production implementations or mocks.

Using dependencies in callbacks#

The dep parameter is made available not only to the LazyPromise constructor callback, but also to all other lazily executed callbacks, namely those you pass to map, catch, catchBoxed, finally, and fromGen:

lazyPromise.map((value, dep: MyDep) => ...);

fromGen(function* (dep: MyDep) {
  ...
});

You must specify the type of the dep parameter explicitly: it will inform the type of the resulting lazy promise.

inject#

You can satisfy the dependency when subscribing, but you can also do it sooner using inject method of a LazyPromise. That method’s callback should return a dependency, but like other lazy callbacks, it can optionally take a dependency as a parameter, allowing dependencies to depend on one another:

declare const upstreamLazyPromise: LazyPromise<MyValue, UpstreamDep>;

// Type inferred as LazyPromise<MyValue, DownstreamDep>.
const downstreamLazyPromise = upstreamLazyPromise.inject(
  (dep: DownstreamDep) => /* a value that satisfies UpstreamDep */,
);

Symbol key pattern#

It’s often convenient, especially when using a dependency across multiple modules, to define it as an object with symbol keys, since you can satisfy multiple such dependencies with a single object without worrying about name clashes:

export const randomSymbol = Symbol("random");
export interface RandomDep {
  [randomSymbol]: () => number;
}

Optional dependencies#

Continuing with the above pattern, a dependency can be optional:

export const randomSymbol = Symbol("random");
export interface RandomDep {
  [randomSymbol]?: () => number;
}

// Type inferred as LazyPromise<number, RandomDep | undefined>
const lazyPromise = fromGen(function* (dep?: RandomDep) {
  return (dep?.[randomSymbol] ?? Math.random)();
});

// No typechecking error even though RandomDep is not provided.
lazyPromise.subscribe({ resolve: console.log });
// Provide RandomDep.
lazyPromise.subscribe({ resolve: console.log }, { [randomSymbol]: () => 0.5 });

InferDep#

This helper type is like Unbox (the equivalent of the native Awaited), but for the dependency type parameter. InferDep<T> gives you the dependency required to satisfy every LazyPromise in T:

// Inferred as A
type Dep1 = InferDep<LazyPromise<..., A>>;

// Inferred as A & B
type Dep2 = InferDep<number | LazyPromise<..., A> | LazyPromise<..., B>>;