Understanding Inversion of Control via a Service Locator in TypeScript
Over the past couple of years, I've spent most of my time developing with Angular. After switching to a new role, I largely shifted to Stencil—a TypeScript compiler that produces web components. Conceptually, it blends Angular with React: it employs decorators to gather metadata and generate output code, while templating relies on JSX/TSX and lifecycle hooks, similar to React. It's a robust tool for constructing design systems.
While building a design system with Stencil, I faced the question of how to manage global state. I wanted to replicate the Inversion of Control approach that I appreciated in Angular. The main advantage of an IoC container is the simplicity of mocking services in tests—there's no need to override the actual module imports in Jest. Although overriding imports can work, it becomes problematic in complex setups where different configurations are needed between test runs; references to a module that exist before the mock is applied will not use the mock, leading to errors.
To address this, I devised a straightforward mechanism for a globally injectable service that remains agnostic to its concrete implementation.
Initially, you outline what the service will handle. From that specification, an interface can be defined, containing all the public methods the service exposes. Take, for example, a basic service for setting and retrieving user data.
Next, a static class named UserServiceInstanceResolver is introduced. This class accepts an instance of a class that implements the interface and holds onto it for later retrieval.
Following that, the actual service implementation is created.
Finally, whenever you need to create the singleton instance of this service, you do so by calling:
The real benefit emerges during testing of components that depend on this instance. Instead of dealing with module mocking, you can easily pass a mock implementation into the UserServiceInstanceResolver.Instantiate method:
This approach satisfied my immediate requirement—a singleton available at the root level. However, with some extensions, the same pattern could accommodate per-component or hierarchical instances, bringing it closer to Angular's dependency injection system.
Thanks for reading. If you found this useful, feel free to show your appreciation.
LE:
Upon reviewing the feedback, it's clear this pattern isn't optimal. For production use, consider established libraries from the TypeScript community, such as tsyringe or decoration-ioc, the latter being a distilled version of the DI used in VSCode. One reason I appreciate writing about new explorations is the immediate feedback it brings—it's a great way to learn and refine approaches. Thanks for that!
