Inject
import { Inject } from "@ecosy/next/inject";
The dependency-injection primitive underneath Route and
Bootstrap, usable on its own.
Inject(map)#
function Inject<Injects extends InjectMap>(injects: Injects): ClassType<Injected<{}, Injects>>
injects |
{ property: ClassToken } — each token is constructed with no arguments. |
| returns | A class whose instances have one property per key, typed as that token's instance. |
Because it returns a class, extends is how dependencies arrive:
class Vendor extends Inject({ fetcher: HttpClient, cache: AppCache }) {
async load(url: string) {
return this.fetcher.get(url); // typed, already constructed
}
}
const vendor = new Vendor();
The subclass gets the injected members plus its own — no wrapper object, no delegation, no container to register with.
If your subclass declares a constructor, call super() first; that is where
the injection happens.
Inject.inject(obj, map)#
function inject<Obj, Injects extends InjectMap>(obj: Obj, injects: Injects): void
Injects onto an existing object instead of through inheritance. This is what
Route uses to put tokens on a request context.
const ctx = { request };
Inject.inject(ctx, { db: Database });
ctx.db.query(/* … */);
Properties are defined as enumerable and configurable, not writable-guarded
— injecting a key that already exists replaces it.
Tokens#
type ClassType<Instance = unknown> = new () => Instance;
type InjectMap = { [key: string]: ClassType };
A token is a class constructible with no arguments. Anything that needs configuration is produced by a factory that captures it and returns such a class:
export function Fetcher(baseURL: string, init = {}): ClassType<Http> {
return class extends Http {
constructor() {
super({ ...init, baseURL, allowedOrigins: [baseURL] });
}
};
}
class Google extends Inject({ fetcher: Fetcher("https://oauth2.googleapis.com") }) {}
The same shape appears across the ecosystem — DiskCache(dir),
Source(fn), Pack(adapter) all return classes,
so all of them drop into an InjectMap directly.
Pass the class, not an instance:
Inject({ cache: DiskCache(".cache") }) // correct
Inject({ cache: new (DiskCache(".cache"))() }) // wrong — not a token
Lifetime#
A token is constructed every time the containing class is. For a route that means once per request.
Anything worth reusing should be a singleton captured by the factory, with the token handing the same instance out:
export function SharedPool(url: string): ClassType<Pool> {
const pool = new Pool(url); // created once
return class {
constructor() { return pool; } // same instance each time
} as ClassType<Pool>;
}
Injected#
type Injected<Context, Injects extends InjectMap> = Context & {
[K in keyof Injects]: Injects[K] extends ClassType<infer Instance> ? Instance : never;
};
The result type — a context plus its injected members. This is what a route handler receives.