Skip to content

Getting Started

xInject is a tiny, dependency-free dependency-injection container. You mark a class with @Injectable, declare what its constructor needs with @Inject, and resolve it with inject - the container builds the class and supplies its dependencies, with full type safety.

Installation

bash
npm install @remotex-labs/xinject
bash
pnpm add @remotex-labs/xinject
bash
yarn add @remotex-labs/xinject

xInject requires Node.js 22 or later and has no runtime dependencies.

Decorators

@Injectable and @Inject are standard decorators. Enable "experimentalDecorators": true in your tsconfig.json. No reflect-metadata and no emitDecoratorMetadata are needed - the token you pass to @Inject is explicit, so xInject works even under bundlers that do not emit decorator type metadata (esbuild, Vite, Bun).

Quick start

ts
import { inject, Inject, Injectable } from '@remotex-labs/xinject';

@Injectable()
class Database {}

@Injectable()
class UserService {
    constructor(@Inject(Database) public db: Database) {}   
}

const users = inject(UserService);
users.db instanceof Database; // true

inject reads the class's registration, resolves the token each @Inject parameter requests, and passes the results to the constructor. A class token that has no explicit provider resolves itself, so a plain class dependency needs no providers entry.

Core ideas

ConceptDescription
@InjectableRegisters a class and records its scope, providers, and optional factory.
@InjectMarks a constructor parameter with the token to resolve for it.
injectResolves and constructs a class, injecting its dependencies.
forceInjectResolves a class but bypasses (and replaces) any cached singleton.
InjectionTokenA typed key for injecting a value that is not a class.
ProvidersHow a token is produced: class, useClass, useFactory, useValue.
Scopessingleton (shared) vs transient (new each time).

Injecting values, not just classes

Interfaces and primitives have no runtime identity, so you inject them through an InjectionToken bound in providers:

ts
import { inject, Inject, Injectable, InjectionToken } from '@remotex-labs/xinject';

const SQL = new InjectionToken<string>('SQL');

@Injectable({ providers: [{ provide: SQL, useValue: 'SELECT * FROM users' }] })
class Database {
    constructor(@Inject(SQL) public sql: string) {}
}

inject(Database).sql; // 'SELECT * FROM users'

Resolution is by token, not by position - the order of providers and of constructor parameters is irrelevant. Mix class and token parameters freely.

No global container

xInject does not keep a shared module-level registry. A class's @Injectable metadata, its @Inject parameter tokens, and its cached singleton are all stored on the class itself. That means:

  • Nothing to import, wire up, or reset between tests - each class carries its own state.
  • Registrations cannot leak between unrelated parts of an app.
  • Resolution is a direct property read on the class, not a map lookup.

Each class's providers form its own registry, so it is resolved independently and never inherits providers from the class that depends on it.

Overriding arguments

Any arguments you pass to inject fill undecorated parameters and take precedence over the @Inject token at each position:

ts
@Injectable()
class Logger {}

@Injectable()
class Service {
    constructor(@Inject(Logger) public logger: Logger) {}
}

const custom = new Logger();
inject(Service, custom).logger === custom; // true - the explicit argument wins

Next steps

Released under the Mozilla Public License 2.0