Inzect is a lightweight injection container for TypeScript and JavaScript.
It is built upon Stage 3 Decorators Proposal.
Install by npm
npm install --save inzector install with yarn
yarn add inzector install with pnpm
pnpm add inzectInzect is built upon the Stage 3 Decorators Proposal.
Please ensure that your tsconfig.json is configured to support Stage 3 decorators.
Specifically, do not enable experimentalDecorators or emitDecoratorMetadata, or simply omit them from the configuration:
Inzect performs Constructor Injection on the constructors of decorated classes.
Marks a class as available for dependency injection. Use this decorator to register a class as a provider that can be injected into other classes.
Parameters
token— Injection Token (see Token). Leave empty to use the decoratedclassas the token.scope(default:Lifecycle.Transient) — Lifecycle scope (see Lifecycle Scope).
Usage
// logger-service.ts
import { Injectable } from 'inzect';
@Injectable()
export class Logger {
public log(message: string): void {
console.log(`[LOG]: ${message}`);
}
}// index.ts
import { container } from 'inzect';
import { Logger } from '~examples/modules/logger';
const logger = container.resolve(Logger);
logger.log('Hello world!');Specifies a dependency to be injected. Use this decorator to inject into class fields or class properties. Or use this decorator to inject into constructor parameters.
-
Inject into class fields or class properties.
Parameters
token— Injection Token (see Token).optional(default:false) — Whether the dependency is optional.
Usage
// app.ts import { Inject } from 'inzect'; import { Logger } from '~examples/modules/logger'; export class App { @Inject(Logger) readonly #logger!: Logger; run(): void { this.#logger.log('Hello world!'); } }
-
Inject into constructor parameters.
Parameters
injects— List ofInject Parameter. Inject parameters are used to inject dependencies into the constructor.
Usage
// app.ts import { Inject } from 'inzect'; import { Logger } from '~examples/modules/logger'; @Inject([Logger]) export class App { readonly #logger: Logger; public constructor(logger: Logger) { this.#logger = logger; } run(): void { this.#logger.log('Hello world!'); } }
Defines the lifecycle scope of a provider. Use this decorator to control how and when instances are created.
Parameters
scope(default:Lifecycle.Transient) — Lifecycle scope (see Lifecycle Scope).
Usage
// database.ts
import { Lifecycle, Scope } from 'inzect';
@Scope(Lifecycle.Singleton)
export class Database {
public async connect(): Promise<void> {
console.log('Database connected');
}
}The Container is the core of the Inzect dependency injection system. It manages provider registration, resolution, and instance lifecycle.
The general principle behind Inversion of Control (IoC) containers is:
you give the container a token, and in exchange you get an instance or value.
Inzect adheres to the Stage 3 Decorators specification of ECMAScript.
Unlike legacy decorators, Stage 3 does not support emitDecoratorMetadata, which means the container cannot infer types from TypeScript metadata.
Therefore, you must explicitly specify the token to inject in most cases, using the @Inject() to decorate (see Inject).
Registers a provider with the container.
Parameters
options.token— Injection Token (see Token).options.provider— Injection Provider (see Provider).options.scope(default:Lifecycle.Transient) — Lifecycle scope (see Lifecycle Scope).
Usage
// index.ts
import { container, Lifecycle } from 'inzect';
import { Logger } from '~examples/modules/logger';
container.register({
token: 'logger',
provider: {
useClass: Logger,
},
scope: Lifecycle.Resolution,
});Unregister a dependency.
Parameters
token— Injection Token (see Token).
Usage
// index.ts
import { container } from 'inzect';
class Service {}
container.register({
token: Service,
provider: {
useClass: Service,
},
});
console.log(container.isRegistered(Service)); // true
container.unregister(Service);
console.log(container.isRegistered(Service)); // falseCheck if a dependency is registered.
It only checks the current container, not the parent containers.
Parameters
token— Injection Token (see Token).
Usage
// index.ts
import { container, Injectable } from 'inzect';
@Injectable()
class Service {}
console.log(container.isRegistered(Service)); // true
console.log(container.isRegistered('some-token')); // falseClears all registered dependencies. It only clears the current container, not the parent containers.
Usage
// index.ts
import { container } from 'inzect';
class Service {}
container.register({
token: Service,
provider: {
useClass: Service,
},
});
console.log(container.isRegistered(Service)); // true
container.clear();
console.log(container.isRegistered(Service)); // falseResolves a dependency.
Parameters
token— Injection Token (see Token).optional(default:false) — Whether the dependency is optional.
Usage
// index.ts
import { container } from 'inzect';
import { Logger } from '~examples/modules/logger';
const LOGGER_TOKEN = Symbol('logger');
container.register({
token: LOGGER_TOKEN,
provider: {
useClass: Logger,
},
});
const logger = container.resolve<Logger>(LOGGER_TOKEN);
logger.log('Hello world!');Resolves a class or token asynchronously. This method is required when any of the class's dependencies (constructor parameters) are registered using asynchronous factory providers.
Unlike resolve(), which works only with synchronous resolution chains, resolveAsync() ensures proper handling of nested or recursive dependencies that rely on asynchronous instantiation.
Parameters
token— Injection Token (see Token).optional(default:false) — Whether the dependency is optional.
Usage
// index.ts
import { container } from 'inzect';
const TIMESTAMP_TOKEN = Symbol('data');
// Fake async data
function fetchTimestamp(): Promise<number> {
return new Promise<number>((resolve) => {
setTimeout(() => {
resolve(Date.now());
}, 1000);
});
}
container.register({
token: TIMESTAMP_TOKEN,
provider: {
useFactory: fetchTimestamp,
},
});
const data = await container.resolveAsync(TIMESTAMP_TOKEN);
console.log(data); // Current timestampCreates a child container that shares access to the registrations of its parent.
When resolving a dependency in the child container, if the token is not registered locally, the lookup will fall back to the parent container.
Usage
// index.ts
import { container, Lifecycle } from 'inzect';
class Service {}
const childContainer1 = container.createChild();
childContainer1.register({
token: 'Service',
provider: {
useClass: Service,
},
scope: Lifecycle.Singleton,
});
const childContainer2 = container.createChild();
childContainer2.register({
token: 'Service',
provider: {
useClass: Service,
},
scope: Lifecycle.Singleton,
});
const service1 = childContainer1.resolve('Service');
const service2 = childContainer2.resolve('Service');
console.log(service1 === service2); // falseSharing access to the registrations of its parent
// index.ts
import { container, Lifecycle } from 'inzect';
class Service {}
container.register({
token: 'Service',
provider: {
useClass: Service,
},
scope: Lifecycle.Singleton,
});
const childContainer1 = container.createChild();
const childContainer2 = container.createChild();
const service1 = childContainer1.resolve('Service');
const service2 = childContainer2.resolve('Service');
console.log(service1 === service2); // trueAn injection token is used to identify a provider.
Such tokens can be:
- Class:
class,abstract class - Primitive:
string,number,boolean,symbol,bigint
A class injection provider is used to provide an instance of a class.
Properties
useClass— Class to provide.
Usage
// index.ts
import { container } from 'inzect';
import { Logger } from '~examples/modules/logger';
container.register({
token: 'logger',
provider: {
useClass: Logger,
},
});A value injection provider is used to provide a value.
Properties
useValue— Value to provide.
Usage
// index.ts
import { container } from 'inzect';
container.register({
token: 'isProduction',
provider: {
useValue: process.env['NODE_ENV'] === 'production',
},
});A factory injection provider is used to provide an instance using a factory function.
Properties
useFactory— Factory function.inject— List ofInject Parameter. Inject parameters are used to inject dependencies into the factory function.
Usage
// index.ts
import { container } from 'inzect';
import { Logger } from '~examples/modules/logger';
container.register({
token: 'database-connection',
provider: {
inject: [
Logger,
{
token: 'isProduction',
optional: true,
},
],
useFactory: (logger: Logger, isProduction?: boolean) => {
if (isProduction) {
return 'production-connection';
}
logger.log('Using development connection');
return 'development-connection';
},
},
});The lifecycle scope of a provider defines how and when instances are created and reused.
Values
Lifecycle.Singleton— One shared instance across the entire application.Lifecycle.Transient— A new instance is created every time the provider is injected.Lifecycle.Resolution— A new instance is created per resolution graph (i.e. percontainer.resolve()call), and reused within that resolution graph.Lifecycle.Container— A new instance is created per container (i.e. percontainer.createChild()call), and reused within that container itself.
Usage
-
Singleton Scope
// index.ts import { container, Lifecycle, Scope } from 'inzect'; @Scope(Lifecycle.Singleton) class Service {} const service1 = container.resolve(Service); const service2 = container.resolve(Service); console.log(service1 === service2); // true
-
Transient Scope
// index.ts import { container, Lifecycle, Scope } from 'inzect'; @Scope(Lifecycle.Transient) class Service {} const service1 = container.resolve(Service); const service2 = container.resolve(Service); console.log(service1 === service2); // false
-
Resolution Scope
// index.ts import { container, Inject, Lifecycle, Scope } from 'inzect'; @Scope(Lifecycle.Resolution) class Service {} @Inject([Service]) class App { @Inject(Service) readonly #service1!: Service; readonly #service2: Service; public constructor(service2: Service) { this.#service2 = service2; } run(): void { console.log(this.#service1 === this.#service2); // true } } const app = container.resolve(App); app.run();
-
Container Scope
// index.ts import { container, Lifecycle } from 'inzect'; class Service {} container.register({ token: 'Service', provider: { useClass: Service, }, scope: Lifecycle.Container, }); const childContainer = container.createChild(); try { childContainer.resolve('Service'); } catch (error) { // It will throw an error because the scope is Container, childContainer can't resolve Service that is registered in global container }
The following features are not yet implemented in Inzect but are planned for future releases:
Support for automatically resolving circular dependencies using lazy proxies or factory wrappers. This requires careful lifecycle management and reference deferral.
Support for [Symbol.dispose] and [Symbol.asyncDispose] to allow cleanup logic for services (e.g., closing DB connections, unsubscribing from streams).
This enables you to hook into the container's internal resolution flow to perform tasks like:
- Logging tokens being resolved
- Measuring resolution time
- Auditing resolved instances
It provides two main hooks:
beforeResolution– called before the container resolves a dependencyafterResolution– called after the instance is resolved
This is useful for logging, metrics, and debugging.
This project is licensed under the MIT License.
You are free to use, modify, and distribute this library in both commercial and non-commercial projects.
Inzect is an open-source project, and contributions are welcome!
Whether it's a bug report, feature request, documentation improvement, or pull request — your input helps make the library better for everyone.
To get started:
- Fork the repository
- Create a feature branch
- Submit a pull request
For major changes or architectural discussions, please open an issue first to discuss your ideas.
Together we can build a more powerful dependency injection system for TypeScript.