Gathering detailed insights and metrics for @lppedd/di-wise-neo
Gathering detailed insights and metrics for @lppedd/di-wise-neo
Gathering detailed insights and metrics for @lppedd/di-wise-neo
Gathering detailed insights and metrics for @lppedd/di-wise-neo
Lightweight, type-safe, flexible dependency injection library for TypeScript and JavaScript
npm install @lppedd/di-wise-neo
Typescript
Module System
Node Version
NPM Version
TypeScript (97.88%)
JavaScript (2.12%)
Total Downloads
656
Last Day
1
Last Week
16
Last Month
656
Last Year
656
MIT License
3 Stars
465 Commits
1 Watchers
2 Branches
2 Contributors
Updated on Aug 06, 2025
Latest Version
0.11.1
Package Id
@lppedd/di-wise-neo@0.11.1
Unpacked Size
431.82 kB
Size
88.50 kB
File Count
9
NPM Version
10.9.2
Node Version
22.17.1
Published on
Aug 06, 2025
Cumulative downloads
Total Downloads
Last Day
0%
1
Compared to previous day
Last Week
-44.8%
16
Compared to previous week
Last Month
0%
656
Compared to previous month
Last Year
0%
656
Compared to previous year
Lightweight, type-safe, flexible dependency injection library for TypeScript and JavaScript
[!NOTE]
di-wise-neo is a fork of di-wise, aiming to provide a simpler yet more powerful API, in part thanks to TypeScript's experimental decorators. Shout out to @exuanbo for the strong foundations!
I've been developing VS Code extensions for a while as part of my daily work. It's enjoyable work! However, extensions always reach that tipping point where feature bloat, and the many different UI interactions which arise from that, make writing, reading, and understanding the codebase a challenge.
Part of the problem is the crazy amount of parameter passing, and the many exported global values floating around waiting to be imported and to generate yet another coupling point.
My background with Java is full of such cases, that have been (partially) mitigated by introducing dependency-injection libraries based on Java's powerful Contexts and Dependency Injection (see Weld, the reference implementation).
So why not apply the same concept to our TypeScript projects?
I've posted on Reddit just to get a feel of what the ecosystem offers, and was
pointed to libraries such as tsyringe, InversifyJS, or Awilix.
I've also explored on my own and discovered redi and di-wise.
What I was looking for is a lightweight solution that offers:
emitDecoratorMetadata
Unfortunately both tsyringe and InversifyJS require reflect-metadata to run correctly. Awilix looks good, but it's probably too much for what I need to do, and it does not support decorators. Plus, the API just didn't click for me.
redi focuses only on constructor injection via decorators, which is nice. However, it falls short when it comes to type safety and resolution scopes: it only supports singletons, with a decorator-based trick to create fresh instances.
And lastly, di-wise. This small library was quite the surprise! Easy to pick up,
no scope creep, injection context support, and full type safety via Angular-like
inject<T>()
functions (that's more like a service locator, but whatever).
The only problems are the slightly overcomplicated API - especially regarding typings - and
the use of ECMAScript Stage 3 decorators, which do not support decorating method parameters :sob:
So what's the right move? Forking the best pick and refactoring it to suite my production needs.
1npm i @lppedd/di-wise-neo
1pnpm add @lppedd/di-wise-neo
1yarn add @lppedd/di-wise-neo
You can find the complete API reference at lppedd.github.io/di-wise-neo
experimentalDecorators
must be enabled in your tsconfig.json
fileArray.flat
, WeakSet
, WeakMap
, Set
, and Map
1// 2// A couple of classes to cover the example 3// 4 5export class ExtensionContext { /* ... */ } 6 7// Both the secret store and the contribution registrar 8// require the extension context to read and set values 9 10export class SecretStore { 11 // We can use function-based injection, which gives us type safety 12 readonly context = inject(ExtensionContext); 13 14 // Or even 15 // constructor(readonly context = inject(ExtensionContext)) {} 16 17 /* ... */ 18} 19 20export class ContributionRegistrar { 21 // We can also opt to use decorator-based constructor injection 22 constructor(@Inject(ExtensionContext) readonly context: ExtensionContext) {} 23 24 /* ... */ 25 26 // Or method injection. The @Optional decorator injects "T | undefined". 27 protected withSecretStore(@Optional(SecretStore) store: SecretStore | undefined): void { 28 if (store?.isSet("key")) { 29 /* ... */ 30 } 31 } 32} 33 34// 35// Using di-wise-neo 36// 37 38// Create a new DI container 39const container = createContainer({ 40 // Optionally override the default "transient" registration scope. 41 // I prefer to use "container" (a.k.a. singleton) scope, but "transient" is the better default. 42 defaultScope: Scope.Container, 43}); 44 45// Register our managed dependencies in the container 46container.register(ExtensionContext) 47 .register(SecretStore) 48 .register(ContributionRegistrar); 49 50// Get the contribution registrar. 51// The container will create a new managed instance for us, with all dependencies injected. 52const registrar = container.resolve(ContributionRegistrar); 53registrar.registerCommand("my.command", () => { console.log("hey!"); });
The Container supports three scope types that determine how and when values are cached and reused.
Creates a new value every time the dependency is resolved, which means values are never cached.
ClassProvider
is instantiated on each resolutionFactoryProvider
is invoked on each resolutionValueProvider
is always returned as-is[!NOTE] When a Transient or Resolution-scoped value is injected into a Container-scoped instance, it effectively inherits the lifecycle of that instance. The value will live as long as the containing instance, even though it is not cached by the container itself.
Creates and caches a single value per resolution graph.
The same value is reused during a single resolution request, but a new one is created
for each separate request.
Creates and caches a single value per container.
If the value is not found in the current container, it is looked up in the parent container,
and so on.
It effectively behaves like a singleton scope, but allows container-specific overrides.
The container allows registering tokens via providers. The generic usage is:
1container.register(/* ... */);
An explicit scope can be specified using the third argument, when applicable.
If omitted, the default scope is Transient.
1container.register(token, provider, { scope: Scope.Resolution });
You can register a class by passing it directly to the register
method:
1container.register(SecretStore);
Alternatively, use an explicit ClassProvider
object - useful when registering
an interface or abstract type:
1const IStore = createType<Store>("Store"); 2container.register(IStore, { 3 useClass: SecretStore, // class SecretStore implements Store 4});
Upon resolving IStore
- which represents the Store
interface - the container
creates an instance of SecretStore
, caching it according to the configured scope.
A lazily computed value can be registered using a factory function:
1const Env = createType<string>("Env") 2container.register(Env, { 3 useFactory: () => isNode() ? "Node.js" : "browser", 4});
The factory function is invoked upon token resolution, and its result is cached according to the configured scope.
A fixed value - always taken as-is and unaffected by scopes - can be registered using:
1const PID = createType<number>("PID"); 2const processId = spawnProcess(); 3container.register(PID, { 4 useValue: processId, 5});
This is especially useful when injecting third-party values that are not created through the DI container.
Registers an alias to another token, allowing multiple identifiers to resolve to the same value.
Using the previous PID
example, we can register a TaskID
alias:
1const TaskID = createType<number>("TaskID"); 2container.register(TaskID, { 3 useExisting: PID, 4});
The container will translate TaskID
to PID
before resolving the value.
The primary way to perform dependency injection in di-wise-neo is through
functions like inject(T)
, injectAll(T)
, optional(T)
, and optionalAll(T)
.
[!TIP] Using injection functions is recommended because it preserves type safety.
All injection functions must be invoked inside an injection context, which stores
the currently active container.
The injection context is available in these situations:
constructor
of a class instantiated by the DI containerFactoryProvider
inject<T>(Token): T
Injects the value associated with a token, throwing an error if the token is not registered in the container.
1export class ProcessManager { 2 constructor(readonly rootPID /*: number */ = inject(PID)) {} 3 4 /* ... */ 5}
If PID
cannot be resolved, a resolution error with detailed information is thrown.
injectAll<T>(Token): T[]
Injects all values associated with a token, throwing an error if the token has never been registered in the container.
1export class ExtensionContext { 2 readonly stores /*: Store[] */ = injectAll(IStore); 3 4 /* ... */ 5 6 clearStorage(): void { 7 this.stores.forEach((store) => store.clear()); 8 } 9}
optional<T>(Token): T
Injects the value associated with a token, or undefined
if the token is not
registered in the container.
1export class ProcessManager { 2 constructor(readonly rootPID /*: number | undefined */ = optional(PID)) {} 3 4 /* ... */ 5}
optionalAll<T>(Token): T[]
Injects all values associated with a token, or an empty array if the token has never been registered in the container.
1export class ExtensionContext { 2 // The type does not change compared to injectAll(T), but the call does not fail 3 readonly stores /*: Store[] */ = optionalAll(IStore); 4 5 /* ... */ 6}
You can also perform dependency injection using TypeScript's experimental decorators.
di-wise-neo supports decorating constructor's and instance method's parameters.
It does not support property injection by design.
@Inject(Token)
Injects the value associated with a token, throwing an error if the token is not registered in the container.
1export class ProcessManager { 2 constructor(@Inject(PID) readonly rootPID: number) {} 3 4 /* ... */ 5 6 // The method is called immediately after instance construction 7 notifyListener(@Inject(ProcessListener) listener: ProcessListener): void { 8 listener.processStarted(this.rootPID); 9 } 10}
If PID
cannot be resolved, a resolution error with detailed information is thrown.
@InjectAll(Token)
Injects all values associated with a token, throwing an error if the token has never been registered in the container.
1export class ExtensionContext { 2 constructor(@InjectAll(IStore) readonly stores: Store[]) {} 3 4 /* ... */ 5 6 clearStorage(): void { 7 this.stores.forEach((store) => store.clear()); 8 } 9}
@Optional(Token)
Injects the value associated with a token, or undefined
if the token is not
registered in the container.
1export class ProcessManager { 2 constructor(@Optional(PID) readonly rootPID: number | undefined) {} 3 4 /* ... */ 5}
@OptionalAll(Token)
Injects all values associated with a token, or an empty array if the token has never been registered in the container.
1export class ExtensionContext { 2 // The type does not change compared to @InjectAll, but construction does not fail 3 constructor(@OptionalAll(IStore) readonly stores: Store[]) {} 4 5 /* ... */ 6}
Sometimes you may need to reference a token or class that is declared later in the file.
Normally, attempting to do that would result in a ReferenceError
:
ReferenceError: Cannot access 'IStore' before initialization
We can work around this problem by using the forwardRef
helper function:
1export class ExtensionContext { 2 constructor(@OptionalAll(forwardRef(() => IStore)) readonly stores: Store[]) {} 3 4 /* ... */ 5}
The library includes four behavioral decorators that influence how classes are registered in the container. These decorators attach metadata to the class type, which is then interpreted by the container during registration.
@Scoped
Specifies a default scope for the decorated class:
1@Scoped(Scope.Container) 2export class ExtensionContext { 3 /* ... */ 4}
Applying @Scoped(Scope.Container)
to the ExtensionContext
class instructs the DI container
to register it with the Container scope by default.
This default can be overridden by explicitly providing registration options:
1container.register( 2 ExtensionContext, 3 { useClass: ExtensionContext }, 4 { scope: Scope.Resolution }, 5);
In this example, ExtensionContext
will be registered with Resolution scope instead.
@Named
Marks a class or injected dependency with a unique name (qualifier), allowing the container to distinguish between multiple implementations of the same type.
1@Named("persistent") 2@Scoped(Scope.Container) 3export class PersistentSecretStorage implements SecretStorage { 4 /* ... */ 5} 6 7// Register the class with Type<SecretStorage>. 8// The container will automatically qualify the registration with 'persistent'. 9container.register(ISecretStorage, { useClass: PersistentSecretStorage }); 10 11// Inject the SecretStorage dependency by name 12export class ExtensionContext { 13 constructor(@Inject(ISecretStorage) @Named("persistent") readonly secretStorage: SecretStorage) {} 14 15 /* ... */ 16}
The container will throw an error at registration time if the name is already taken by another registration.
@AutoRegister
Enables automatic registration of the decorated class when it is resolved, if it has not been registered beforehand.
1@AutoRegister() 2export class ExtensionContext { 3 /* ... */ 4} 5 6// Resolve the class without prior registration. It works! 7container.resolve(ExtensionContext);
@EagerInstantiate
Sets the default class scope to Container and marks the class for eager instantiation upon registration.
This causes the container to immediately create and cache the instance of the class at registration time, instead of deferring instantiation until the first resolution.
1@EagerInstantiate() 2export class ExtensionContext { 3 /* ... */ 4} 5 6// ExtensionContext is registered with Container scope, 7// and an instance is immediately created and cached by the container 8container.register(ExtensionContext);
[!WARNING] Eager instantiation requires that all dependencies of the class are already registered in the container.
If they are not, registration will fail.
Testing is an important part of software development, and dependency injection is meant to make it easier.
The container API exposes methods to more easily integrate with testing scenarios.
resetRegistry
Removes all registrations from the container's internal registry, effectively resetting it to its initial state.
This is useful for ensuring isolation between tests.
1describe("My test suite", () => { 2 const container = createContainer(); 3 4 afterEach(() => { 5 container.resetRegistry(); 6 }); 7 8 /* ... */ 9});
dispose
Another way to ensure isolation between tests is to completely replace the DI container after each test run.
The di-wise-neo container supports being disposed, preventing further registrations or resolutions.
1describe("My test suite", () => { 2 let container = createContainer(); 3 4 afterEach(() => { 5 container.dispose(); 6 container = createContainer(); 7 }); 8 9 /* ... */ 10});
di-wise-neo is a fork of di-wise.
All credits to the original author for focusing on a clean architecture and on code quality.
2025-present Edoardo Luppi
2024-2025 Xuanbo Cheng
No vulnerabilities found.