Yet Another Type Level Arithmetics
DI is getting more popularity in JavaScript. Though, those solution is far way beyond JSR-330 compatible libraries in terms of type safety and performance.
This snippet strives to dig more type-safety from TS’ type system. However, it can hardly achieve equivalent type safety like Java’s counterpart, e.g., Dagger, Guice.
Solution
This DI snippet can, ideally, ensure every binding is resolved in compile time, which is a hard task for other DI solution. The main idea is an Injector
can statically know its current binding, and judge whether dependencies of a new added binding can be already resolved by itself. Since dependency graph is a DAG, there exists a topological sorting order that every binding’s dependency can be resolved solely by those of preceding bindings . So once the injector is created and bindings are attached to it, we can assert that the dependencies can be resolved.
Say, there is a minimal example to illustrate this:
1 |
|
To implement this, Injector has a shadow type Base
that indicates resolved bindings. When new binding is added to injector, compiler will verify the new coming constructor/function will only depend on classes the injector has already resolved. Concretely, every argument in newly added constructor must be a subtype of Base
.
1 | type Ctor = new (...args: Base[]): T |
Base
is a large union type storing all binding types, so every resolved type is a subtype of Base
. And bind
will return a binder
that has toClass / toFactory
method which further returns an injector whose resolved binding is a union of the previous binding type and the newly added binding type. Hence, after bind ... toClass
, the injector has a new class appended to its resolved type list.
The implementation and test can be found at Github Gist.
Problem
But TS’ type system does not allow a full-fledged DI in this way.
First, runtime types are erased. One must annotate dependency for function in
toFactory
method.toClass
is better because TS supportsemitDecoratorMetadata
. (maybe resolved in TS2.0). TS’ specific metadata implementation is also problematic. For cyclic dependent classes, at least one class’ annotation isundefined
(ES3/5), or the script is crashed before it can run (ES6). Because metadata is attached to class declaration, in cyclic case there must be one class is used before it’s declared.TypeScript has a double-edged sutructural type system. To fully exploit DI’s type check, user has to add a
private
brand
field to every injectable class. This is not a good UI, though.But even metadata is not enough. Runtime type data is first-order (in type-system’s view), that is, every type is represented by its constructor, no generic information is emitted. To work around this, token is introduced.
Token alleviates runtime type-system’s weakness, and enables binding multiple implementations to one single type. It also introduces more problem this DI wants to resolve in the first place. To work around point 1, we attached runtime types to constructor. Binding token will make type system think a type has resolved, but a following binding may not resolve it in runtime because it depends on constructor to find resolution.
1 | injector |
Also, tokens with same types cannot avoid this.
The workaround is, well, abusing string literal type. So every token is different at type-level. This requires users to type more types, and casts string literal from string type to string literal type. (TS’s generic inference does not have something like T extends StringLiteral
so that T
is inferred as string literal type)
Also, the toClass
and toFactory
signature should differentiate what can be resolved by constructor and by token. This is technically possible, just override these signature to support distinguishing between token and constructor. But the number of resulting overriding is exponential to the number of argument. 2 ^ n, where n is the number of arguments.
Conclusion
To fully support type-safe DI and higher performance, a compiler extension or a code generator is needed. Java’s DI relies on annotations and code generation.
Maybe Babel can do this right now. But TypeScript still needs a long way to go for a customizable emitter.
Source
1 | import 'reflect-metadata' |