A dead-simple, modern, and lightweight (~2kb) Dependency Injection library for TypeScript.
Stimshot draws inspiration from established Dependency Injection (DI) frameworks like InversifyJS, Tsyringe, and InjectionJS. However, a key limitation shared by these libraries is their reliance on legacy mechanisms that are incompatible with modern TypeScript best practices.
The Problem with Reflection-Based DI: Existing solutions commonly depend on experimental decorators and the reflect-metadata library. This forces users to enable emitDecoratorMetadata in their tsconfig.json. This approach is problematic because it relies on TypeScript's compile-time type information, which is inherently erased during compilation. This often results in runtime errors, as demonstrated by this example that builds but fails at execution. Most DI libraries fail to correctly handle the new import type syntax. Because this syntax explicitly signals a type-only import, reflection-based dependency resolution fails, leading to frustrating runtime issues.
Stimshot was built with a different philosophy:
- Explicit & Guess-less: No magic strings, no fragile type-reflection. You explicitly ask for what you need. What you see is what you get.
- Weightless: The entire library is one small file with no dependencies.
- Modern: Built for modern TypeScript (5.0+) using standard Stage 3 Decorators.
- Simple APIs: The API surface is tiny, intuitive, and easy to understand.
This library does not use or require reflect-metadata or experimentalDecorators or emitDecoratorMetadata like other DI libraries.
| stimshot | InversifyJS | Tsyringe | InjectionJS | |
|---|---|---|---|---|
| reflect-metadata | ✅ Not needed | Yes |
Yes |
Yes |
| Library size | ✅ |
|||
| Modern TypeScript Support | ✅ Yes | No | Partial | No |
| Extensive feature set | Only core features | ✅ | ✅ | ✅ |
Using a DI pattern is a cornerstone of modern software design. It helps you build:
- Modular Code: Classes don't create their own dependencies; they just receive them. This makes your code loosely coupled and easier to manage.
- Testable Code: DI makes testing a breeze. You can easily "replace" a real database or HTTP service with a fake (mock) version during tests.
- Clean Architecture: It helps enforce the Single Responsibility Principle, leading to a codebase that's easier to scale and maintain.
- ✅ Modern TypeScript: Uses standard Stage 3 decorators (available in TS 5.0+).
- ❌ No reflect-metadata: No hacks or reliance on fragile type information.
- ❌ No experimentalDecorators: Uses modern typescript's stage 3 decorators.
- 🧠 Simple API: A minimal set of 5 functions: @shared, @fresh, resolve, replace, and reset.
- 🧪 Testable by Design: replace and reset make unit testing trivial and safe.
npm install stimshot
The design is simple:
- Decorate your classes with
@shared()(for singletons (shared instances)) or@fresh()(for new instances). - Resolve your dependencies using
resolve().
import { resolve, shared } from 'stimshot';
@shared() // Means singleton
class Chip {
public readonly name = "tensor";
}
@shared()
class Screen {
public readonly dpi = 441;
}
@shared()
class Phone {
private readonly chip = resolve(Chip); // Define dependency like this
constructor(
public readonly screen = resolve(Screen) // Or like this
){}
getSpecs() {
return `Phone with ${this.chip.name} chip and ${this.screen.dpi} DPI screen.`;
}
}
// --- Then to make Phone instance ---
const phone = resolve(Phone); // Don't use 'new Phone()' method!
phone.getSpecs();stimshot makes it trivial to replace dependencies in your tests.
import { resolve, shared, replace, reset } from 'stimshot';
@shared()
class Chip {
public readonly name = "tensor";
}
@shared()
class Phone {
private readonly chip = resolve(Chip); // Define dependency like this
getSpecs() {
return `Phone with ${this.chip.name} chip`;
}
}
// --- In your test setup ---
// I want to test the Phone class logic without using the real Chip class.
// This is a good practice in unit testing to isolate the class under test.
// So now Chip class will not interfere with our Phone tests.
// Create a mock implementation for Chip
@shared()
class MockChip {
public readonly name = "mock-tensor";
}
replace(Chip, { useClass: MockChip }); // Replace Chip with MockChip
const phone = resolve(Phone); // Resolve Phone, which now uses MockChip
console.log(phone.getSpecs()); // Outputs: Phone with mock-tensor chipYou can also replace with a simple object (useValue) or a function (useFactory).
@shared(): Class decorator. Registers the class as a "singleton." A single instance will be created on the first resolve and shared for all subsequent calls (This is what you usually want).@fresh(): Class decorator. A new instance will be created every time it is resolved.
resolve(Token): Resolves a dependency from the container. Token is the class constructor (e.g., resolve(Database)).replace(Token, options): Used for testing. Replaces the registered Token with a mock implementation.options.useClass: Replaces with another registered class.options.useValue: Replaces with a specific value (e.g., a mock object).options.useFactory: Replaces with a function that returns the instance.
reset(): Used for testing. Resets the entire container, clearing all shared instances and removing all replacements, restoring the original configuration.
MIT © 2025 Vajahath Ahmed
