Gyaku (逆, "inversion") is a tiny, modern DI container for TypeScript, built around await using.
- Type-safe registry chain
- No decorators, no
reflect-metadata - Sync and async factories under one API
- Unknown deps and duplicate keys caught at compile time
- Parallel resolution along the dependency graph
- Parallel, graph-aware disposal via
await using - Auto-cleanup of partially built services on failure
Symbol.asyncDisposeandSymbol.disposeboth honored
Requires Node.js 24+.
npm install @gyaku/diimport { createRegistry } from "@gyaku/di";
const createGreeter = ({ name }: { name: string }) => ({
say: () => console.log(`hello, ${name}`),
});
const registry = createRegistry()
.value("name", "gyaku")
.service("greeter", ["name"], createGreeter);
await using services = await registry.resolve();
services.greeter.say();
// hello, gyakuRunnable examples: examples/.
Using an AI coding agent? Install the gyaku skill with npx skills:
npx skills add mkizka/gyakuReturns an empty, immutable registry.
Registers a pre-built value, keeping its type as-is.
createRegistry().value("config", { port: 3000 });Registers a sync or async factory that receives only the dependencies listed in deps.
const registry = createRegistry()
.value("config", { port: 3000 })
.service("logger", createLogger)
.service("server", ["config", "logger"], createServer);Swaps a registered factory in place, following the same rules as .service —
deps can differ from the original registration. The replacement's return
type must still satisfy the original contract.
const testRegistry = productionRegistry.replaceService(
"db",
["config"], // can differ from the original "db" registration's deps
createStubDb,
);Swaps a registered service with a pre-built instance, keeping the original return type.
const testRegistry = productionRegistry.replaceValue("db", stubDb);Resolves the graph and returns Promise<Services & AsyncDisposable>; factories run in parallel, await using disposes in reverse along the graph, and any failure auto-disposes what was already created.
All errors extend GyakuError.
RegistryError— invalid argument to.service/.value/.replaceService/.replaceValue.ResolveError—.resolve()failed.errorsmixesServiceFactoryErrorandServiceDisposeError.DisposeError—Symbol.asyncDisposefailed.errorsisServiceDisposeError[].
Each inner error has .key (the service that failed) and .cause (the original throw).
import { ResolveError, ServiceFactoryError } from "@gyaku/di";
try {
await using services = await registry.resolve();
} catch (error) {
if (error instanceof ResolveError) {
for (const e of error.errors) {
console.error(e.key, e.cause);
}
}
}- Re-registering a key throws; use
.replaceService/.replaceValueto replace. thenis reserved (would make the services object look thenable).- Services object has a null prototype, so keys like
__proto__are safe.
gyaku's first-class factory is a function taking a single deps object,
({ logger, db }) => .... These helpers adapt code that doesn't fit that
shape — positional functions and classes — without rewriting it.
Wraps a function that takes positional arguments. deps lists those
parameters in order.
const createRepo = (logger: Logger, db: Db) => ({
find: (sql: string) => db.query(sql),
});
createRegistry().service("repo", ["logger", "db"], asFunctionArgs(createRepo));Wraps a class whose constructor takes a single deps object, so you skip the
(deps) => new Foo(deps) wrapper. Fully type-safe.
class Greeter {
constructor(private deps: { logger: Logger }) {}
}
createRegistry().service("greeter", ["logger"], asClass(Greeter));Like asFunctionArgs, but for a class with a positional constructor.
deps lists the constructor parameters in order.
class Greeter {
constructor(logger: Logger, db: Db) {}
}
createRegistry().service("greeter", ["logger", "db"], asClassArgs(Greeter));asClass<Interface>()(Class) registers the service as Interface instead of
the concrete class (asClassArgs supports the same form). This matters for
.replaceService / .replaceValue: a replacement must match the registered
type, and matching a concrete class down to its #private fields is usually
impossible. Pin to an interface and a stub only has to satisfy that interface.
interface UserRepository {
find(id: number): Promise<User | undefined>;
}
class UserRepositoryImpl implements UserRepository {
constructor(private deps: { db: Db }) {}
find(id: number) {
return this.deps.db.findUser(id);
}
}
const registry = createRegistry()
.service("db", createDb)
// pinned to UserRepository, not UserRepositoryImpl
.service(
"userRepository",
["db"],
asClass<UserRepository>()(UserRepositoryImpl),
);
// the stub only has to satisfy UserRepository
const testRegistry = registry.replaceValue("userRepository", {
find: async (id) => ({ id, name: "stub" }),
});MIT