I’ve wired up enough services in TypeScript to know the pain of the usual contenders: sprawling globals, decorator-heavy DI containers, and hand-rolled factories that grow brittle with every new dependency. Wyr is the tool I always wished existed: a stateless, typesafe registry that treats dependency wiring as data. Here’s the philosophy, how it compares, and why it’s now my default.
The Contenders (and why they fall short)
Global scope “service registries” – Toss everything onto
globalThis
, hope the import order is kind, and pray Next.js doesn’t optimize something away. It’s fast to throw together but impossible to reason about. You can’t diff it, you can’t snapshot it, and tests become a tightrope walk over ambient state.NestJS-style containers – Great if you love classes and decorators. Not so great when you need value-based services, dynamic wiring, or precise types. Mocks must match classes, scoping rules are opaque, and circular dependencies surface at runtime with stack traces from the abyss.
Manual wiring – Still better than the other two: at least you know what’s happening. But it’s death by a thousand constructors. Every refactor means touching multiple files, and there’s zero compile-time help to stop you from forgetting a dependency. Tests become a copy-paste festival.
I wanted the consistency of declarative wiring with the clarity of explicit code. Something I could diff, test, and reason about without magic.
Philosophy: registries, not containers
wyr is a registry of bindings. That’s it. No hidden scope, no lifecycle annotations, no mysterious module graph. You describe a dependency as a tuple of keys, and TypeScript ensures the graph is valid before you ever run the test suite.
-
Pure data – Behind the scenes every binding is just
{ deps, factory }
. Cloning a registry is a spread operation. - No runtime mutation – Each bind returns a new registry; nothing mutates under your feet.
- Graphs over containers – Dependencies are explicit edges. You can inspect them, merge them, or snapshot them.
- Deterministic wiring – Resolution is a single function that walks the graph, validates it, and resolves nodes in parallel.
It’s the inversion-of-control model I wanted: explicit graphs, compile-time guarantees, zero hidden global state.
Safety baked in
Two things still give me a little thrill every time I run them:
Compile-time missing dependency detection
If you bind an API that depends onrepo$
, TypeScript will refuse to compile untilrepo$
is bound. You see the missing key, the context, and the offending registry before you open Vitest.Compile-time cycle detection
Accidentally create a loop when refactoring? You get a descriptive type error that points to the cycle. No more runtime deadlocks or stack overflows.
All of this lives in the type system—no custom lint rules, no code generation.
The fresh take
Wyr intentionally leaves out the trappings of traditional containers:
- No scopes, modules, providers, beans, or decorators.
- No eager singletons or lifecycle annotations.
- No global container instance to mutate (or forget to reset in tests).
You get four core operations:
-
bind().toValue()
– lock in a concrete value. -
bind().toFunction([...deps], fn)
– describe how to build a service given dependencies. -
wire
/wireTuple
/wireRecord
– resolve services deterministically. -
snapshot([...keys])
– materialize a subset into a standalone registry with no dependencies.
That’s the entire API surface. Easy to teach, easy to audit, easy to test.
The people who pushed me to ship it
Wyr wouldn’t exist without a handful of TypeScript “typelevel magicians” I met in the community—folks who maintain their own libraries, routinely bend the type system to their will, and even ship PRs straight into TypeScript itself. Watching them operate made me want to contribute something that felt equally deliberate and uncompromising. Wyr is my attempt to join that conversation: a library that leans on the type system, explains itself through data, and holds up under scrutiny.
Why I built it (for me)
Every project that outgrew the “just import a module” phase seemed to devolve into container chaos or brittle wiring scripts. I wanted:
- A way to prove the dependency graph was sound at compile time.
- Declarative wiring that read like a build plan, not a constructor chain.
- Testability without ceremony: spinning up a registry for a spec should be a one-liner.
- Deterministic, parallel resolution without sacrificing clarity.
wyr is the result. I’ve been using it to assemble services that are simultaneously flexible and predictable. When I snapshot a subset of the graph for a microservice test harness or a CLI utility, I know exactly what’s included and how it was built.
Want to try it?
Grab it on npm:
npm install wyr-ts
Define your keys, bind your services, and let TypeScript keep you honest. The README and GitHub Wiki cover getting started, architecture, and how to snapshot subgraphs for tests.
No more “wait, where is this actually coming from?” Just registries, graphs, and confidence.
Top comments (0)