Haywire
Compile-time checked dependency injection for TypeScript: no decorators, no reflect-metadata, no global state.
Missing bindings, mismatched types and duplicate registrations are all TypeScript errors, so a container that compiles has everything it needs.
import { bind, createContainer, createModule, identifier, singletonScope } from 'haywire';
interface Logger {
info(message: string): void;
}
const loggerId = identifier<Logger>();
const dbUrlId = identifier<string>().named('dbUrl');
class UserService {
constructor(
private readonly logger: Logger,
private readonly dbUrl: string
) {}
}
const appModule = createModule(bind(loggerId).withFactory(() => console).scoped(singletonScope))
.addBinding(bind(dbUrlId).withInstance('postgres://localhost/app'))
.addBinding(bind(UserService).withDependencies([loggerId, dbUrlId]).withConstructorProvider());
const container = createContainer(appModule);
const userService = container.get(UserService);Hover over any identifier in the examples on this site to see its inferred type. Every example is type-checked against Haywire's published types when the site is built.
Features
- Wiring mistakes are type errors: A missing dependency, a duplicate binding, the wrong id passed to
container.get(), or a nullable value injected into a non-nullable parameter all fail to build. - Plain JavaScript: Constructor injection with no decorators,
experimentalDecoratorsorreflect-metadata. Works with private fields, and in plain JavaScript too. - Type-based ids, not strings:
identifier<T>()tells twostringdependencies, such as a database URL and password, apart at the type level. - Real-world lifecycles: Singleton, request and transient scopes, opt-in async initialization, explicit circular dependencies, runtime values like an HTTP request supplied per call, and
await usingdisposal of what the container created. - Immutable and independent: Modules never mutate, and containers share no global registry, so tests can build their own and swap in fakes with a type-checked
replaceBinding().
Compared to
- InversifyJS, TSyringe, TypeDI and NestJS's injector rely on decorators and string or token keys, and find missing bindings at runtime.
- Awilix avoids decorators, but resolves dependencies by name at runtime.
Haywire is the only one of these where a container that compiles is guaranteed to have every binding it needs.
Related
Packages
- haywire-launcher
Run a Haywire-built entry script in one line, building the container only when the script runs.
Used in demos
- Read-through cache
BentoCache on Valkey: a memory tier kept in sync, stale-while-revalidate, one load per key across instances, and sharding over several nodes with weighted rendezvous hashing.
- OpenAPI
A notes API from spec to database: an OpenAPI 3.1 document built from Juniper schemas, genever types for a typed client and a Hono server that validates every request and response, and Drizzle on Postgres with a database per test.
- MCP server
Making the notes API easy for AI agents that connect to services: tools shaped for tasks, an OAuth resource server with step-up scopes, and token exchange to call the API as the agent’s user.
- CLI for agents
Making the notes API easy for AI agents with a shell: one short command per task, logging in once from the API’s URL, with tokens, refresh and errors handled inside.