Skip to main content

The one-keyword bug: when your library's types vanish across a package boundary

ยท 5 min read

Here are two services. Same body, same return type, same everything a linter or a reviewer would look at:

export function Lights({ logger }: TServiceParams) { /* ... */ }
export const Lights = ({ logger }: TServiceParams) => { /* ... */ };

Swap one for the other inside a published library, pull that library into a downstream app through implies (or a rollup), and the runtime is identical โ€” but with the arrow, the app silently loses every type the library was supposed to bring. params.lighting goes from fully-typed to gone. Nothing errors. The build is green. The types just aren't there.

I hit this building library composition for core. The cause turned out to live in one of the quieter corners of TypeScript's declaration emitter, and it's worth a writeup โ€” because the rule people reach for ("arrows are the problem") is wrong, and the real rule is one line.

Composing libraries without a type black hole: LibraryGroup, implies, and the circular dependency I had to *not* solve

ยท 8 min read

Once a Digital Alchemy app grows past a handful of libraries, the libraries array stops being a list and becomes a chore. Each plugin-like group is itself several libraries, and the application has to name every one of them, plus every transitive dependency, in one flat array. Miss one and boot throws MISSING_DEPENDENCY. It works, but you're hand-maintaining a dependency graph that the libraries already know.

So core now has opt-in primitives for composing that list โ€” and getting them to carry types across a package boundary led me straight into a circular dependency I deliberately left unsolved. That second part is the interesting one.

Building a Plugin Registry with Digital Alchemy

ยท 5 min read

There's a pattern that shows up repeatedly in production Digital Alchemy applications: a central service that manages a set of interchangeable backends, where each backend is its own library and self-registers at startup. No hardcoded coupling between the core and any specific backend. Adding a new adapter is one file and one line.

This post walks through how it works.