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