Skip to content

Repository files navigation

Inversify Props

Dependency injection for TypeScript, without the boilerplate. inversify-props wraps InversifyJS so you can register a class once and inject it as a property (or constructor parameter) with a single @inject() — no manual ids, no fluent binding ceremony. Framework-agnostic: works with Vue, LitElement, vanilla TS, or anything else. Built on inversify 8.

Using React? Try the companion package inversify-hooks, a thin useInject layer on top of this one.

inversify-props.ckgrafico.com

npm version npm downloads GitHub license GitHub last commit GitHub issues

logo


Table of contents

Why

Plain InversifyJS asks you to manage service identifiers and write fluent bindings for every dependency. inversify-props does that for you: it derives ids from class names automatically and lets you inject by property. You probably don't need it if you want the full power of inversify (custom scopes everywhere, factories, multi-injection) — it's intentionally a thin convenience layer for the common "inject my services" case.

Installation

npm install inversify-props

Built on inversify 8, which is pulled in for you — no separate reflect-metadata install needed. Type definitions and both ESM and CommonJS builds ship with the package.

Note: inversify 8 is ESM-first. Bundlers (Vite, webpack, etc.) need nothing special; consuming it via require() from plain CommonJS Node needs Node 20.19+ or 22+.

TypeScript configuration

inversify-props uses legacy decorators, so enable experimentalDecorators and keep useDefineForClassFields off:

{
  "compilerOptions": {
    "target": "es2020",
    "lib": ["es2020", "dom"],
    "moduleResolution": "bundler",
    "experimentalDecorators": true,
    "useDefineForClassFields": false
  }
}

⚠️ If useDefineForClassFields is true, an injected property's class field shadows the injected getter and reads back undefined. It defaults to false when target is below ES2022.

Quick start

import { cid, container, inject } from 'inversify-props';

// 1. Define a class + interface
interface IService1 {
  method1(): string;
}
class Service1 implements IService1 {
  method1() {
    return 'method 1';
  }
}

// 2. Register it (usually in app.container.ts), before anything resolves
container.addSingleton<IService1>(Service1);

// 3. Inject it anywhere
class MyComponent {
  @inject() service1!: IService1;
}

// ...or resolve it directly
const service1 = container.get<IService1>(cid.IService1);

⚠️ If you use a minifier/obfuscator, configure it to preserve class and function names — see Troubleshooting.

Registering dependencies

Register on the shared container before resolving. Three lifetimes are available:

container.addSingleton<IService1>(Service1); // one shared instance (most common)
container.addTransient<ILogger>(Logger);     // a new instance on every resolve
container.addRequest<IUnitOfWork>(UnitOfWork); // one instance per request scope

The generic (<IService1>) is just the compile-time type. The runtime id is derived from the class name and cached under both Service1 and IService1, which is why cid.IService1 works.

Injecting dependencies

Inject as a property — the id is inferred from the property name (camelCase, PascalCase and leading _ are all handled):

export class MyOtherService {
  @inject() private service1: IService1;
  @inject() private _service1: IService1;
  @inject() private Service1: IService1;
}

Constructor injection infers the id from the parameter name:

export class MyOtherService {
  constructor(@inject() private exampleService: IExampleService) {}
}

Custom ids

Don't want auto-generated ids? Pass your own:

container.addSingleton<IService1>(Service1, 'MyService1');

export class MyComponent {
  @inject('MyService1') service1!: IService1;
}

Your own container

The library ships a ready-to-use container, but you can create and install your own:

import { Container, setContainer } from 'inversify-props';

setContainer(new Container());

Testing with mocks

The recommended pattern, before each test: reset the container, re-register dependencies, then mock what the test needs.

import { cid, mockSingleton, resetContainer } from 'inversify-props';

beforeEach(() => {
  resetContainer();
  containerBuilder(); // your registrations
  mockSingleton<IHttpService>(cid.IHttpService, HttpServiceMock);
});

mockTransient and mockRequest have the same signature. resetContainer() unbinds everything.

API reference

Export Description
container The shared container instance (auto-created).
container.addSingleton<T>(Class, id?) Register a single shared instance.
container.addTransient<T>(Class, id?) Register a new instance per resolve.
container.addRequest<T>(Class, id?) Register one instance per request scope.
container.get<T>(id) Resolve a dependency by id.
cid Cache of generated ids, e.g. cid.IService1.
inject / Inject Property/parameter decorator for injecting dependencies.
injectable Class decorator marking a class as injectable.
mockSingleton / mockTransient / mockRequest Replace a registered id with another implementation (testing).
resetContainer() Unbind everything from the container.
getContainer() / setContainer(opts) Access or replace the underlying InversifyJS container.
Container The container class, if you want your own instance.

Runnable examples

Two minimal demos live in examples/, each showing a service that injects another service via @inject():

# from the repo root — build the library first
npm install && npm run build

cd examples/vue   # or examples/lit
npm install
npm run dev

Use it as an agent skill

This repo ships an Agent Skill so AI coding agents (Claude Code, Cursor, etc.) know how to wire up DI with this library. Install it with npx skills:

npx skills add CKGrafico/inversify-props

Troubleshooting

Symptom Cause & fix
Injected property is undefined useDefineForClassFields is true. Set it to false (or keep target below ES2022).
Works in dev, breaks in production The minifier mangled class names, so cid.IXxx is undefined. Enable keepNames (esbuild/Vite) or keep_classnames + keep_fnames (Terser/Uglify). Vue CLI uses Terser under the hood — set it via configureWebpackoptimization.minimizer.
No matching bindings found The service wasn't registered, or registration ran after it was resolved. Register first.
Decorators throw at runtime experimentalDecorators is off in your tsconfig.json.

Credits

Built on InversifyJS. Licensed under MIT.

About

Wrapper of Inversify to inject your dependencies in the components, made with TypeScript and compatible with Vue, React and other component libraries.

Topics

Resources

Stars

86 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages