TypeDrop

2026-07-17 Challenge

2026-07-17 Hard

Typed Dependency Injection Container

You're building the service-locator core for a backend framework. Modules register factories under typed token keys, declare their dependencies on other tokens, and the container must resolve the full dependency graph at runtime — detecting circular dependencies, enforcing that every dependency is registered before resolution, and returning a fully-typed resolved instance — all with zero `any`.

Goals

  • Define `Token<T>` as a branded phantom type backed by a plain `symbol`, and implement `createToken<T>()` so tokens for different types are mutually incompatible.
  • Implement `ResolvedTuple<Tokens>` as a homomorphic mapped type that extracts the phantom type from each `Token<X>` in a tuple, and wire it into `ServiceFactory` and `provide()` so dependency types flow through without any annotations at call sites.
  • Implement `Container.register` and `Container.resolve` with singleton caching, recursive transitive dependency resolution, and cycle detection via a `visiting` set — all surfaces through the discriminated-union `ResolveResult<T>` type.
  • Implement `Container.unregister` and `Container.isRegistered`, ensuring that unregistering a token also evicts its cached singleton so a subsequent re-registration starts fresh.
challenge.ts
// Core types at a glance

/** Nominal token carrying a phantom type T */
type Token<T> = symbol & { readonly __tokenType: T };

/** Maps a tuple of Token<X> → tuple of X */
type ResolvedTuple<Tokens extends ReadonlyArray<Token<unknown>>> = {
  [K in keyof Tokens]: Tokens[K] extends Token<infer U> ? U : never;
};

/** Typed factory descriptor */
interface ServiceFactory<T, Deps extends ReadonlyArray<Token<unknown>> = []> {
  readonly deps: Deps;
  readonly create: (...args: ResolvedTuple<Deps>) => T;
}

type ResolveResult<T> =
  | { readonly ok: true;  readonly value: T }
  | { readonly ok: false; readonly reason: "NOT_REGISTERED" | "CIRCULAR_DEP" | "FACTORY_THREW"; readonly message: string };

// Main API
declare class Container {
  register<T, Deps extends ReadonlyArray<Token<unknown>>>(token: Token<T>, factory: ServiceFactory<T, Deps>): this;
  resolve<T>(token: Token<T>): ResolveResult<T>;
  isRegistered<T>(token: Token<T>): boolean;
  unregister<T>(token: Token<T>): boolean;
}
Hints (click to reveal)

Hints

  • For `Token<T>`, the phantom field only needs to exist at the type level — `Symbol() as Token<T>` is the one place an assertion is acceptable, or you can use a helper overload that hides it. Focus on making the *public* API assertion-free.
  • In `provide()`, annotate `deps` as `Deps` (constrained to `ReadonlyArray<Token<unknown>>`) and let TypeScript infer `Deps` from the passed-in `as const` tuple — then `ResolvedTuple<Deps>` will automatically type the `create` parameters.
  • For cycle detection, pass a *new* `Set` into the top-level `resolve` call and `add`/`delete` the current token around each recursive `resolveInner` call — this correctly handles diamond dependencies (shared non-circular deps) without false positives.

Or clone locally

git clone -b challenge/2026-07-17 https://github.com/niltonheck/typedrop.git