← Back to archive

Repo of the Day

tada5hi/eldin

Published: Mar 17, 2026

Open repository ↗

A lightweight, type-safe dependency injection container for TypeScript with scoped lifetimes and hierarchical containers.

Summary

eldin is a TypeScript dependency injection container that emphasizes type safety through TypedToken<T> phantom types rather than decorators or runtime metadata reflection. It supports value, factory, and async factory providers with singleton, transient, and scoped lifetimes, and allows child and scoped containers for hierarchical resolution. The library is published under the MIT license.

What it is useful for

eldin is useful when a TypeScript application or library needs a DI container but the team wants to avoid decorator- and reflection-based systems, for example to keep build configuration simple, support runtimes that lack reflect-metadata, or maintain clearer type inference. The typed token approach means container.resolve(Token) returns the correct inferred type without manual generics, reducing a class of type errors during wiring.

Engineers working on modular services, plugin systems, or request-scoped code can use eldin's scoped lifetimes to isolate per-request state, while child containers let sub-systems override parent registrations for testing or feature flags. Multi-bindings cover cases where several implementations of a concept (handlers, validators, formatters) should be resolved together. Async factory providers address dependencies that need asynchronous setup, such as database connections or authenticated clients, with resolveAsync safely handling concurrent calls to the same singleton.

How engineers can use it

Installation is documented as npm install eldin --save.

Basic usage follows a three-step pattern:

  1. Define typed tokens with new TypedToken<T>('Name'). Each token has a unique Symbol id, so name collisions across types are impossible.
  2. Register providers on a Container using useValue, useFactory, or useAsyncFactory.
  3. Resolve with container.resolve(Token) (sync) or await container.resolveAsync(Token) (async). resolveAsync works uniformly on sync providers, value providers, and async factories.

Lifetimes are passed as the third argument to register, defaulting to singleton; transient creates a new instance each resolve, and scoped requires a scope created via container.createScope(). For safer code, tryResolve and tryResolveAsync return a Result<T> discriminated union instead of throwing, which is useful in optional-dependency code paths. Tests can construct a Container with useValue overrides or skip the container entirely and instantiate dependencies directly.

Documented limitations to keep in mind: 'scoped' is not supported for multi-bindings and throws a ContainerError; resolving a scoped registration outside a scope also throws; and resolve() throws if the provider is async, requiring resolveAsync() instead. The README contains the full sync/async API tables and the IContainer interface for those who want to program against the abstraction.