Socket
Book a DemoInstallSign in
Socket

@morgan-stanley/needle

Package Overview
Dependencies
Maintainers
4
Versions
27
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@morgan-stanley/needle

A small & lightweight dependency injection container for use in multiple contexts like Angular, React & node.

0.3.36
latest
Source
npmnpm
Version published
Weekly downloads
75
-67.53%
Maintainers
4
Weekly downloads
 
Created
Source

@morgan-stanley/needle

Lifecycle Active npm Build Status codecov Known Vulnerabilities NPM NPM

What is Needle?

Needle is a lightweight & powerful dependency injection container for supporting the development of universal code with full semantic runtime injection. It helps you increase the testability of your applications as well as decoupling your code more effectively

Installation

npm install @morgan-stanley/needle

TypeScript

Required Typescript version: > 3.4

The library depends on TypeScript's support for decorators.

Why use this?

  • Simple & lightweight
  • Can be used in many different JavaScript contexts: node, browsers, angular, react or vanilla js
  • Provides a non-invasive way to stand up a tree of dependencies
  • Increases your code testability
  • Support semantic versioning injection

Feature support

FeatureSub-featureDetailsStatus
Decorator supportUsing '@decorators' to signal behaviorFull Support
Optional decoratorsSupporting decorator free injectionFull Support
TypeScript supportFull TypeScript support with type safetyFull Support
Global configurationAbility to configure global settings in the containerFull Support
Semantic InjectionAbility to respect semantic versioning in all injectable typesFull Support
CacheCaching of injectablesFull Support
Cache manipulationAbility to directly manipulate the cacheFull Support
Scoping supportCaching support in scoped injectorsFull Support
MetricsTracking injectables in the systemFull Support
Auto trackingZero config tracking modelFull Support
Activation trackingTracking when a type is first constructedFull Support
Activation ownersTracking what type is responsible for constructing an InjectableFull Support
Resolution statisticsDetails of how often a type has been resolved etcFull Support
Creation costCost in time to construct the InjectableFull Support
Dependency countsNumber of dependencies a given type has in its constructorFull Support
Scoping supportAre metrics tracked in scoped injectorsFull Support
Metrics manipulationCan developers manipulate the metric dataFull Support
TokenisationDoes the DI library support tokenisationFull Support
Decorator supportCan you define tokens using '@decorators'Full Support
API supportCan you define tokens using an APIFull Support
String tokensCan I use strings as tokensFull Support
Symbol tokensCan I use Symbols as tokensFull Support
Multiple tokensCan I register multiple tokens for a single injectableFull Support
Token overridingCan I override existing token registrationsFull Support
Unique token enforcementCan I enforce unqiue tokensFull Support
Scoping supportAre tokens supported in scoped injectorsFull Support
StrategiesDoes the DI library support injecting multiple injectables into a given constructorFull Support
Decorator supportCan I use @decorators to register a strategyFull Support
API supportCan I use the API to register a strategyFull Support
String tokensCan I register strategies using stringsFull Support
Symbol tokensCan I register strategies using SymbolsFull Support
Scoping supportAre strategies supported in scoped injectorsFull Support
FactoriesDoes the DI library support factory construction typesFull Support
Decorator supportCan I use @decorators to resolve a factoryFull Support
API supportCan I use API to resolve a factoryFull Support
Scoping supportAre factories supported in scoped injectorsFull Support
Auto factoriesCan all types be used as FactoriesFull Support
Parameter profilingCan I control constructor parameters explicitlyFull Support
Lazy InjectionDoes the DI library support lazy dependency injectionFull Support
Decorator supportCan I use @decorators to register/resolve a lazy injectableFull Support
API supportCan I use the API to register/resolve a lazy injectableFull Support
Scoping supportAre lazy injectables supported in scoped injectorsFull Support
Optional InjectionDoes the DI library support optional constructor params for injectionFull Support
Decorator supportCan I use @decorators to resolve optional injectableFull Support
API supportCan I use the API to register/resolve a optional injectableFull Support
Scoping supportAre optional injectables supported in scoped injectorsFull Support
Instance InjectionDoes the DI library support registering instances against a typeFull Support
API supportCan I use the API to register an instance of a type for injectionFull Support
Scoping supportAre instances supported in scoped injectorsFull Support
Value InjectionDoes the DI library allow for registering a value for injection (Non-injectable types)Full Support
Intrinsic valuesCan I register intrinsic types such as Date, Regex, NumberFull Support
AOT valuesCan I eagerly supply the value for the value injectionFull Support
JIT valuesCan I compute the value at point of injectionFull Support
Dynamic valuesCan I recompute the value being injected on each resolutionFull Support
Custom ConstructionDoes the DI library support construction external to the library itselfFull Support
Bespoke type constructionCan I create my own constructor for a given typeFull Support
Global bespoke constructionCan I create a global constructor for all typesFull Support
Abstract type constructionCan I create a constructor for abstract base typesFull Support
Scoping supportAre custom constructors supported in scoped injectorsFull Support
Caching strategiesDoes the DI library support scoped injection contextsFull Support
PersistentCan injectables be cached indefinitelyFull Support
No cachingCan I stop the injector caching my injectablesFull Support
Weak ReferencesCan I store injectables using weak references in the cacheFull Support
Idle cachingCan have my injectables evicted on the cache based on a given timeoutFull Support
Conditional cachingCan I evict items from the cache if a given condition is metFull Support
Hierarchical injectionDoes the DI library support scoped injection contextsFull Support
String scope namesCan I use strings for scope namesFull Support
Symbol scope namesCan I use Symbols for scoped namesFull Support
Registration overridingCan I override ancestral registration in my scopeFull Support
DisposalCan I destroy a scopeFull Support
DestroyCan I hook into destroy so that I can clean up my resourcesFull Support
Scope lookupCan I find a scope easily using its name or id.Full Support
Scope inheritanceCan scopes extend other scopesFull Support
InterceptionDoes the DI library support interceptorsFull Support
Decorator supportCan I register interceptions using @decoratorsFull Support
API supportCan I register interceptions using the APIFull Support
Before construction interceptionCan I intercept a given type before its constructedFull Support
After construction interceptionCan I intercept a given type after its constructedFull Support
Injection delegationCan I delegate all construction to another DI library.Full Support

Injectable basics

Decorators & Metadata

This library performs runtime type introspection in order to determine what types it should construct. There are two modes you can use for metadata auto-generated or explicit. Lets take a look at each, first lets start with explicit metadata.

Explicit Metadata

When using explicit metadata we can use either the decorator or the registration API. Below is an example of how we provide the metadata using the decorator.

import { Injectable } from '@morgan-stanley/needle';

@Injectable()
class Pet {}

//Explicit metadata via the metadata property on the decorator.
@Injectable({ metadata: [Pet] })
class Owner {
    constructor(pet: Pet) {}
}

And if we prefer to use the registration API we can provide the metadata as follows.

//Explicit metadata using the registration API
getRootInjector()
    .register(Owner, { metadata: [Pet] })
    .register(Pet);

Note, if you are injecting a token or a strategy etc into a constructor you may not have the type available to you. In this case you can use following.

import { Injectable, METADATA } from '@morgan-stanley/needle';

@Injectable({ metadata: [METADATA.token] })
class Owner {
    constructor(@Inject('my-pet') pet: IPet) {}
}

Auto-generated metadata

TypeScript has support for being able to generate the metadata for you as part of your compilation. You can enable this feature in your tsconfig.json file as follows.

{
    "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

Once enabled TypeScript will emit special JS code which depends on a polyfill library called reflect-metadata. To use this approach you must install this polyfill with

npm install reflect-metadata

Once installed you must then import this module as the very first import on the root of your application.

import 'reflect-metadata';

After this, needle will then adopt a strategy of looking for explicitly defined metadata first and if not found falling back to the TypeScript generated metadata.

Creating an injectable type

The easiest way to make a type injectable is to decorate it with the @Injectable decorator. All types by default decorated in this way will be available for injection in any runtime context.

import { Injectable } from '@morgan-stanley/needle';

@Injectable()
class Pet {}

@Injectable()
class Owner {
    constructor(pet: Pet) {}
}

While decorators are recommended, you can also achieve the same using the Injector API. You gain access to this API by importing the getRootInjector function.

import { getRootInjector } from '@morgan-stanley/needle';

class Pet {}

class Owner {
    constructor(pet: Pet) {}
}

//Equivalent to decorator
getRootInjector()
    .register(Owner, { metadata: [Pet] })
    .register(Pet);

Resolving injectables

In order to resolve an instance of your injectable you have a couple of options. You can use the getRootInjector function which you can import from the main package. This function returns an instance of the Injector API.

import { getRootInjector } from '@morgan-stanley/needle';

const myThing = getRootInjector().get(Owner);

Alternatively, you can import the get function directly.

import { get } from '@morgan-stanley/needle';

const myThing = get(Owner);

Both examples map to the same underlying implementation and use the root injector to resolve an instance. Resolving the same type twice will result in the same instance being serviced from the cache.

import { getRootInjector, get } from '@morgan-stanley/needle';

const owner1 = getRootInjector().get(Owner);
const owner2 = get(Owner);

console.log(owner1 === owner2); //True

All child dependencies (in this case Pet) will be automatically resolved for the Owners constructor.

Tokens

Tokens allow us to provide a marker to the injector whereby the type we are going to be injecting either cannot be imported or we wish to use an interface instead. Every injectable in the system can be registered with either zero or more tokens. A single type can register itself against multiple tokens. Tokens can be defined using a string or symbol

Registering with tokens

The simplest way to register your type against a token is to use the tokens array defined in the @Injectable decorator. here we have a type GeographyStudent who defines a string geography-student upon which this type can be resolved.

import { Injectable } from '@morgan-stanley/needle';

@Injectable({
    tokens: ['geography-student'],
})
export class GeographyStudent extends Student {}

The API equivalent of this registration is shown below.

import { getRootInjector } from '@morgan-stanley/needle';

getRootInjector().register(GeographyStudent, { tokens: ['geography-student'] });

As stated, you are not limited to just one 1 token per type. Simply add additional tokens to the list if you require more.

import { Injectable } from '@morgan-stanley/needle';

@Injectable({
    tokens: ['geography-student', 'student'],
})
export class GeographyStudent extends Student {}

Registering Symbols for tokens

Using strings as tokens for most teams is perfectly acceptable, however often in large code bases it is possible to run into naming collisions. In order to resolve this issue you can instead adopt Symbols instead to define your tokens. Below is an example of two registrations where the Symbol names overlap but will not pollute each other when resolutions are made as Symbols are unique.

import { getRootInjector } from '@morgan-stanley/needle';

const pricingSymbol1 = Symbol.for('pricing');
const pricingSymbol2 = Symbol.for('pricing');

getRootInjector().configuration.allowDuplicateTokens = false;

getRootInjector()
    .register(PricingServiceV1, { tokens: [pricingSymbol1] })
    .register(PricingServiceV2, { tokens: [pricingSymbol2] }); //No exception thrown as Symbols are unique

Resolving by token

To resolve a type by token we can make use of the @Inject decorator. In the constructor of a given injectable we can mark one of the parameters with @Inject providing a token which we wish to resolve. Note, the parameter type does not need to match the type of the injected value. This is what allows us to use either interfaces or a sub type as a replacement for the real type.

@Injectable()
export class GeographyTeacher extends Person {
    constructor(@Inject('geography-student') public student: Student) {
        super();
    }
}

The API equivalent of this registration is shown below.

import { getRootInjector } from '@morgan-stanley/needle';

export class GeographyTeacher extends Person {
    constructor(public student: IStudent) {
        super();
    }
}

const argumentIndex = 0;

getRootInjector()
    .register(GeographyTeacher)
    .registerParamForTokenInjection('geography-student', GeographyTeacher, argumentIndex);

Token overriding

It is often the case that you may want to resolve a different type instance as a replacement for say a default one. For example, say you have a Pricing service which for new customers you want to use the new pricing models but for existing customers you will use the old pricing model.

Using token injection we follow a last in first out (LIFO). Therefore, the last Injectable to be registered to a given token is the one that will be resolved.

Note: the configuration must be set to allowDuplicateTokens for this to be possible.

import { getRootInjector, Injectable } from '@morgan-stanley/needle';

getRootInjector().configuration.allowDuplicateTokens = true;

@Injectable(
    tokens: ['pricing']
)
export class PricingServiceV1 implements IPricing {}

@Injectable(
    tokens: ['pricing']
)
export class PricingServiceV2 implements IPricing  {}

@Injectable()
export class CustomerPricing {
    constructor(@Inject('pricing') private pricing: IPricing) {
        console.log(pricing instanceof PricingServiceV2) // true
        super();
    }
}

The API equivalent of this registration is shown below.

import { getRootInjector } from '@morgan-stanley/needle';

getRootInjector().configuration.allowDuplicateTokens = true;

const argumentIndex = 0;

getRootInjector()
    .register(PricingServiceV1, { tokens: ['pricing'] })
    .register(PricingServiceV2, { tokens: ['pricing'] })
    .register(CustomerPricing)
    .registerParamForTokenInjection('pricing', CustomerPricing, argumentIndex);

Unique token enforcement

If you wish to restrict duplicate tokens in the system you can control this using the configuration. Note, the default is already set to false.

import { getRootInjector } from '@morgan-stanley/needle';

getRootInjector().configuration.allowDuplicateTokens = false;

getRootInjector()
    .register(PricingServiceV1, { tokens: ['pricing'] })
    //Exception thrown here
    .register(PricingServiceV2, { tokens: ['pricing'] });

Strategies

Creating strategies

Strategies allow us to register multiple type providers against a given strategy key and then inject an array of all the strategies in the given consumer class. An injectable type can both exist as a strategy and pure injectable at the same time.

Creating strategies can be achieved using the @Injectable decorator or the API. Both approaches make use of the strategy property on the injectable config.

Registering strategies

import { Injectable } from '@morgan-stanley/needle';

interface IStrategy {}

@Injectable({
    strategy: 'work-strategies',
})
export class Strategy1 implements IStrategy {}

// tslint:disable-next-line:max-classes-per-file
@Injectable({
    strategy: 'work-strategies',
})
export class Strategy2 implements IStrategy {}

or registering via the API would look like this.

import { getRootInjector } from '@morgan-stanley/needle';

getRootInjector()
    .register(Strategy1, {
        strategy: 'work-strategies',
    })
    .register(Strategy2, {
        strategy: 'work-strategies',
    });

To avoid naming conflicts that can occur with strings, you can also use symbols for your strategy names. Below is an example of this using the injector API.

import { getRootInjector } from '@morgan-stanley/needle';
const strategySymbol = Symbol.for('work-strategies');

getRootInjector()
    .register(Strategy1, {
        strategy: strategySymbol,
    })
    .register(Strategy2, {
        strategy: strategySymbol,
    });

Resolving strategies

When it comes to injecting lists of strategies we can use the @Strategy decorator to mark that we expect an array of strategies. You can register consumers of strategies using this decorator or the API.

import { Injectable, get } from '@morgan-stanley/needle';

@Injectable()
export class StrategyConsumer {
    constructor(@Strategy('work-strategies') public workStrategies: IStrategy[]) {}
}

const instance = get(StrategyConsumer);

console.log(instance.workStrategies.length); // 2 strategies

Or using the API we can resolve a list of strategies in the following way.

import { getRootInjector } from '@morgan-stanley/needle';

const strategies = getRootInjector().getStrategies('work-strategies');

console.log(strategies.length); // 2 strategies

Factories

It is often the case that you need to be able to construct types with specific context or dependencies. For these use cases you can rely on factories.

Registering a Factory

All types registered with the container can be used as factories. There is no special registration required.

Resolve a Factory

There are two ways to resolve a factory. Explicitly using the API or via the @Factory decorator. Below are examples of both types of resolution.

import { getRootInjector } from '@morgan-stanley/needle';

const carFactory = getRootInjector().getFactory(Car);

console.log(carFactory); // Defined

Example of decorator.

@Injectable()
class CarManufacturer {
    constructor(@Factory(Car) private carFactory: AutoFactory<typeof Car>) {}
}

In each case the consumer will be returned an AutoFactory. The AutoFactory provides a type safe create method in order to construct a new instance of the target type.

@Injectable()
class Engine {}

@Injectable()
class Car extends Vehicle {
    constructor(
        public engine: Engine,
        public numberOfDoor = 2,
    ) {
        super('Car');
    }
}

const factory: AutoFactory<typeof Car> = getRootInjector().getFactory(Car);
const carWithFourDoors = factory.create(undefined, 4);
const carWithSuperPowerfulEngine = factory.create(new SuperPowerfulEngine());

If you prefer not to pass undefined to the factory, there is also a named constant AUTO_RESOLVE which can be used instead.

factory.create(AUTO_RESOLVE, 4);

If you would not like the injector to auto resolve the value for engine and you wanted to actually return null or undefined you can use well known injector values (UNDEFINED_VALUE, NULL_VALUE ) to achieve this.


@Injectable()
import { NULL_VALUE, UNDEFINED_VALUE} from '@morgan-stanley/needle';

const factory:  AutoFactory<typeof Car> = getRootInjector().getFactory(Car);
const carWithEmptyEngine = factory.create(UNDEFINED_VALUE, 4);
const carWithNoEngine = factory.create(NULL_VALUE);

carWithEmptyEngine.engine === undefined //True
carWithNoEngine.engine === null //True

IMPORTANT: You can only pass undefined to constructor params which either support injection or default value. Type safety must be adhered to so SuperPowerfulEngine in this case must extend Engine type to be valid to the compiler.

Lazy injection

In certain situations, constructing the entire dependency tree can either be expensive or alternatively might introduce side effects you want to avoid. In those cases Lazy injectables can be useful. Lazy injectables provide a placeholder injection type of LazyInstance<T> which will only construct the target injectable when its value property is read.

Registering a Lazy injectable

All types registered with the container can be used with lazy injection. There is no special registration required.

Resolve a LazyInstance

There are two ways to resolve a Lazy. Explicitly using the API or via the @Lazy decorator. Below are examples of both types of resolution.

import { getRootInjector } from '@morgan-stanley/needle';

const carLazy = getRootInjector().getLazy(Car);

console.log(carLazy); // Defined

We can trigger realization of the target injectable value by reading the lazy's value property.

const carInstance = carLazy.value;

We can also check to see if the lazy's value has been generated by reading the hasValue property.

let hasValue = carLazy.hasValue; //False;
const carInstance = carLazy.value;
hasValue = carLazy.hasValue; //True;

We can use the @Lazy decorator to signal to the injector that we would like a lazy to be provided in place of the real injectable.

@Injectable()
class CarManufacturer {
    constructor(@Lazy(Car) private carLazy: LazyInstance<typeof Car>) {}
}

Optional injection

In some environments it will not always be the case that an injectable type has been registered with the injector. For these scenarios you can leverage the @Optional decorator which will allow the injector to resolve undefined if no matching registration can be found.

Registering an Optional Injectable

All constructor types can be used with optional injection. There is no special registration required.

Resolve an optional injectable

We can use the @Optional decorator to signal to the injector that we would like it to resolve undefined if no registrations can be found. Below is an example of a constructor for a Car type which supports optional storage.

@Injectable()
class Car {
    constructor(@Optional() private storage?: Storage) {
        console.log(storage); //Undefined
    }
}

You can also resolve an optional injectable using the getOptional method on the injector api.

const car = injector.getOptional(Car); //Undefined

Resolve and optional token

The @Optional decorator and the getOptional method also accept tokens to optionally inject:

@Injectable()
class Car {
    constructor(@Optional('storageToken') private storage?: Storage) {
        console.log(storage); //Undefined
    }
}
const car = injector.getOptional('carToken'); //Undefined

Register instance

There are sometimes where you do not want the injection container to create the type. Instead you want to take an already existing instance and register it against a type. For this you can use registerInstance on the injector.

import { getRootInjector } from '@morgan-stanley/needle';

const vehicle = new Vehicle('Bike');

getRootInjector().registerInstance(Vehicle, vehicle);

const instance = get(Vehicle);

console.log(instance === vehicle); // True

External Resolution Strategies

There are times where you may require more granular control of a specific types construction. This may be because you want to resolve the the type from a different injection container, the type may not be Newable consider the case of an abstract class or for a variety of other reasons. In order to support this, needles injectable config provides a resolution property which can be used to specify your own external resolution strategy.

Registering a type for external resolution

If you want to entirely own the process of constructing a given type you can define an ExternalResolutionStrategy which will be used in place of needles construction logic. Below shows an example of registering our own resolution strategy against a given type using the decorator approach. The resolution takes a resolver function where you can perform your custom construction and an additional flag (cacheSyncing) signalling to needle if it should store the result in its internal cache.

@Injectable({
    resolution: {
        resolver: (_injector, _args) => new SuperCar(),
        cacheSyncing: true,
    },
})
class SuperCar {}

The same can be achieved using the injector API.

import { getRootInjector } from '@morgan-stanley/needle';

getRootInjector().register(SuperCar, {
    resolution: {
        resolver: (_injector, _args) => new SuperCar(),
        cacheSyncing: true,
    },
});

Registering abstract types for external resolution

Some types cannot be constructed directly, these types instead require that we use a subtype which is considered Newable. This is commonly the case where we have an abstract base class that we want to inject that into other types using the base class, but at the same time we need to provide a concrete instance.

In the example below we provide an example of this whereby we have an abstract Car class which can be registered to a sub type, in this case SuperCar.

Note: The resolution property accepts a shorthand version whereby you provide just a compatible type for the super type. If this is used, needle will automatically do the type substitution for you. This makes overriding a base type very simple.

@Injectable({
    resolution: SuperCar,
})
abstract class Car {}

Or using the injector API

import { getRootInjector } from '@morgan-stanley/needle';

getRootInjector().register(Car, { resolution: SuperCar });

In the section under Global Configuration you can learn about how you can use External Resolution Strategies to delegate construction globally and provide fallback strategies when performing interop with other container systems.

Register Value

JavaScript has a number of intrinsic types that you may wish to inject directly into a constructor without the need for wrapping in a higher type. These types include Array, Boolean, Date, Error, Function, JSON, Number, RegExp and String. In order to support direct injection of these types we can use the registerValue method found on the injector. The registerValue method only takes one parameter, the injection configuration object. There are two ways to resolve the value, you can either provide a value at point of registration or you can use a resolution strategy to resolve at point of use. Below are some examples to illustrate how this works.

Registering with a AOT value

@Injectable()
class SecurityContext {
    constructor(@Inject('security-token') public securityToken: string) {}
}

// Tokens are used the same as for other types.
getRootInjector().registerValue<string>({
    tokens: ['security-token'],
    value: 'TmVlZGxlIFByb2plY3Q=',
});

Registering with a JIT computed value value

getRootInjector().registerValue<string>({
    tokens: ['security-token'],
    value: {
        cacheSyncing: true,
        resolver: (_injector) => Encryption.resolveUserContextToken(),
    },
});

If you want the value to mutate on each request, you can set cacheSyncing to false.

Note: As values have no associated type upon which to decorate, you can only use the Injector API to register values.

Metrics tracking

The injector tracks metrics about your injectable types during runtime. There are a range of different values captured and these are stored in the metrics provider which is accessible via the Injector type. The data is store as records and the below type shows the information captured.

export interface IMetricRecord {
    /**
     * The type who's metrics are being tracked
     */
    type: any;
    /**
     * First activation time
     */
    activated: Date;
    /**
     * What type constructed this type. (Defaults to self if bare resolution)
     */
    activationTypeOwner: any;
    /**
     * The number of times this type has been resolved
     */
    resolutionCount: number;
    /**
     * The last time this type resolved
     */
    lastResolution: Date;
    /**
     * The number of types this type depends on based on constructor signature.
     */
    dependencyCount: number;
    /**
     * The time it took to construct this type
     */
    creationTimeMs: number;
}

Reading the metric data

There are a number of ways to read the metric data. To access via the injector instance simply do the following.

import { getRootInjector } from '@morgan-stanley/needle';

const metrics = getRootInjector().metrics;

You can easily dump the data to the console using the following.

getRootInjector().metrics.dump();

You can reset the metrics by calling the reset method.

getRootInjector().metrics.reset();

You read the metrics for a specific type by using the getMetricsForType method.

getRootInjector().metrics.getMetricsForType(MyType);

You an read all the metric data by reading the data property.

const records: IMetricRecord[] = getRootInjector().metrics.data;

Caching strategies

Needle provides several flexible caching strategies for managing your injectables, allowing you to fine-tune memory usage and optimize for different runtime environments. The available caching strategies are:

  • Persistent: The default strategy. Once an injectable is created, it remains cached for the application's lifetime.
  • No-cache: Disables caching entirely. Each resolution creates a new instance.
  • Weak references: Uses JavaScript's WeakRef to cache injectables, enabling garbage collection when no strong references remain.
  • Idle: Evicts injectables from the cache after a specified period of inactivity. The timeout resets on each access.
  • Conditional: Lets you define a custom predicate function to determine when an injectable should be evicted from the cache, evaluated after each resolution.

These strategies give you granular control over resource management and can be tailored to fit the needs of both high-performance and resource-constrained applications.

Persistent

By default, the persistent cache strategy is applied automatically—no additional configuration is needed. However, if you want to be explicit and ensure that your injectable always uses the persistent strategy (regardless of global settings), you can specify it directly as shown below.

//Using the decorator
@Injectable({ cacheStrategy: 'persistent' })
class NaughtyTurtle {}

//Using the registration API
injector.register(NaughtyTurtle, { cacheStrategy: 'persistent' });

No cache

The no-cache option tells Needle to skip caching entirely, so a fresh instance is created every time the injectable is resolved. This is useful when you need stateless or short-lived objects that shouldn't be reused. Example:

//Using the decorator
@Injectable({ cacheStrategy: 'no-cache' })
class NaughtyTurtle {}

//Using the registration API
injector.register(NaughtyTurtle, { cacheStrategy: 'no-cache' });

Weak references

The weak-reference cache strategy tells Needle to store your injectable instance using JavaScript's WeakRef. This allows the garbage collector to reclaim the instance's memory if there are no strong references to it elsewhere in your application. As a result, you are not guaranteed to receive the same instance on subsequent resolutions if the instance has been collected, Needle will create a new one. Because garbage collection is non-deterministic, use this strategy when you want to minimize memory usage and don't require persistent instances.

//Using the decorator
@Injectable({cacheStrategy: 'weak-reference'})
class NaughtyTurtle {}

//Using the registration API
injector.register(NaughtyTurtle, { cacheStrategy: 'weak-reference'' });

Idle

The idle cache strategy automatically removes your injectable from the cache after a specified period of inactivity. You can configure the timeout in milliseconds. Each time the injectable is accessed, the timer resets so the instance remains cached as long as it is used within the timeout window. This is useful for managing memory in scenarios where objects should only persist while actively in use.

//Using the decorator
@Injectable({ cacheStrategy: { type: 'idle', timeout: 500 } })
class NaughtyTurtle {}

//Using the registration API
injector.register(NaughtyTurtle, { cacheStrategy: { type: 'idle', timeout: 500 } });

Conditional

The conditional cache strategy allows you to evict items from the cache based on a custom condition. You provide a predicate function that receives the instance and returns true if the item should be evicted. This gives you fine-grained control over cache behavior, enabling scenarios like time-based, state-based, or environment-based eviction. Example:

//Using the decorator
@Injectable({
    cacheStrategy: {
        type: 'conditional',
        predicate: (instance) => new Date().getDay() === 3 /*its Wednesday so evict*/,
    },
})
class NaughtyTurtle {}

//Using the registration API
injector.register(NaughtyTurtle, {
    cacheStrategy: {
        type: 'conditional',
        predicate: (instance) => new Date().getDay() === 3 /*its Wednesday so evict*/,
    },
});

Scoped injection

Hierarchical injection (aka Scoped injection) is the ability to create new child injection scopes which descend from our primary root injector. There are many times when you may require scoped injection, normally they are associated with a given context in our app domains and provide the ability to deviate from the global registrations defined in the root injector.

Scoped injectors inherit their ancestors registrations by default, and then can override those with their own or add additional registrations as required.

Scoped injectors can also be created from other scopes, therefore allowing you to build complex hierarchies which can model your domain accurately.

Creating a scope

To create a new injection scope we can call the createScope method from any Injector instance. When we create the new scope we must provide a name for the new scope.

const scopedInjector = getRootInjector().createScope('my-scoped-injector');

const amIScoped = scopedInjector.isScoped(); //True

Scope Resolution

By default scoped injectors inherit their parents registrations. Any updates to the parent scopes registrations automatically flow to the child scopes.

Therefore if a parent has defined a registration and we try and resolve that type from our scoped injector, then the type instance will be provided without error.

This is true no matter how deep the scope hierarchy is, as the resolution process walks up the tree until a valid registration is found. Example below.

const rootInjector = getRootInjector();

//Single registration
rootInjector.register(Child);

//Create 2 levels of scope
const level2Injector = rootInjector.createScope('level-1').createScope('level-2');

//Instance of child (Serviced from root injector)
const child = level2Injector.get(Child);

Scope overrides

The key reason to create a new scope is in order to provide overrides for a particular context in your applications. During the resolution process for a given type, the injector will first look towards its local registrations and if it finds a match will use that over any parent registrations. Therefore you can easily replace registrations from the hierarchy with your own and create instances localized to your scope and all child scopes within it.

const rootInjector = getRootInjector();

//Single registration
rootInjector.register(Child);

//Create 2 levels of scope
const level2Injector = rootInjector.createScope('level-1').createScope('level-2');

//Create our scoped registrations
level2Injector
    .register(Child) //override root registration
    .registerInstance(Teacher, new Teacher('History'));

const child1 = rootInjector.get(Child);
const child2 = level2Injector.get(Child);
const teacher = level2Injector.get(Teacher);

child1 === child2; // False

rootInjector.get(Teacher); //Fails as no registration in parent scopes

As a scoped injector is no different to the root injector, the full registration API is available. Therefore you can create registrations of any kind.

Finding a scope

If you want to resolve a scope you can do this either using the id or name of the scope. Below is an example.

const rootInjector = getRootInjector();

const level2Injector = rootInjector.createScope('level-1');

rootInjector.getScope(level2Injector.id);
rootInjector.getScope('level-1');

Scope disposal

As scoped injectors sit in a tree they can be disposed of easily by calling dispose either on the scope directly or a parent scope.

const rootInjector = getRootInjector();

//Single registration
rootInjector.register(Child);

//Create 2 levels of scope
const level2Injector = rootInjector.createScope('level-1').createScope('level-2');

const level1 = rootInjector.getScope('level-1');
const level2 = rootInjector.getScope('level-2');

level1.destroy();

level1.isDestroyed(); //True;
level2.isDestroyed(); //True;

Destroy hooks

When an injection scope is destroyed needle will look to see if injectables in that scopes cache implement IDestroyable. If cache references are found, needle will invoke the needle_destroy function. This provides a perfect place for you to perform necessary clean up. Example below.

export class Child implements IDestroyable {
    public isDestroyed = false;

    public needle_destroy(): void {
        this.isDestroyed = true;
    }
}

const rootInjector = getRootInjector();

//Create a scoped injector
const scopedInjector = rootInjector.createScope('my-new-scope');

scopedInjector.register(Child);

const child = scopedInjector.get(Child);
console.log(child.isDestroyed); //False

scopedInjector.destroy();

console.log(child.isDestroyed); //True

Interception

Needle provides support for interception of construction using interceptors. Interceptors provide the ability for developers to hook into a types construction both immediately before and after a type is instanced. This technique is useful when you need to configure an instance before that instance is injected into downstream consumers. For example, if we had a Car type which injected an Engine, we may wish to call the engine.tune() function before giving to the car instance. Interceptors are considered global inside of needle. Therefore if you create multiple scopes each instance being constructed will pass through the same set of interceptors.

Creating an interceptor

To create an interceptor is simple, we simply implement an interface called IConstructionInterceptor in our class. The interface is generic and there we can provide the Type that we wish to target as a generic param. Below is an example interceptor which implements the interface for the Engine type.

export class EngineInterceptor implements IConstructionInterceptor<typeof Engine> {
    public readonly target: typeof Engine = Engine;
    public beforeCreate(context: IInjectionContext<typeof Engine>): void {
        console.log(context);
    }
    public afterCreate(instance: Engine, context: IInjectionContext<typeof Engine>): void {
        console.log(instance);
        console.log(context);
    }
}

The interface requires we implement 3 members

  • target - The type we wish to intercept
  • beforeCreate - A method that will be invoked directly before the type instanced and after all its constructor args are resolved.
  • afterCreate - A method that will be invoked immediately after the target type was instanced.

Each method call will receive an injection context object which provides information about the context of injection. This includes information such as the injector instance resolving the type, the configuration used during construction and an array of constructor args.

Registering an interceptor

There are two ways to register an interceptor in the system, you can either decorate your class with @Interceptor(). The decorator approach essentially makes you interceptor an injectable so that you can inject other dependencies into it. The other approach is to use the injector API and provide an instance manually. Note: Decorated interceptors will be constructed at point of registration.

//Decorated
@Interceptor()
class EngineInterceptor {}

//Explicit
injector.registerInterceptor(new EngineInterceptor());

Note, regardless of the scope of the injector all interceptors will be registered with the the root injector.

Global configuration

Max tree depth

When constructing a tree of dependencies the hierarchy can get very deep, this is especially so if a circular reference is encountered. Determining if this is the case can be difficult which is where maxTreeDepth can help. Setting this value (defaults to 500) will set a max limit on the depth of the tree being created. If the limit is reached an exception will be thrown.

import { getRootInjector } from '@morgan-stanley/needle';

getRootInjector().configuration.maxTreeDepth = 1000;

Track metrics

By default the injector will track common metric information about types in the system. This includes information such as first activation time, number of resolutions, cost of construction etc. You can disable metrics tracking by setting the trackMetrics flag to false.

import { getRootInjector } from '@morgan-stanley/needle';

getRootInjector().configuration.trackMetrics = false;

Default cache strategy

You set the global defaultCacheStrategy that will be used for injectables which do not define a local caching strategy via the injection config. For Example

import { getRootInjector } from '@morgan-stanley/needle';

getRootInjector().configuration.defaultCacheStrategy = 'persistent';

Default metadata modes

You set the global default metadataMode that will be used when resolving metadata for injection. The options are explicit, where metadata is resolved the metadata array only, reflection where the metadata will attempt to be resolved using the reflect-metadata polyfill, or both which is the default and will attempt to resolve explicit first then fallback to reflection.

import { getRootInjector } from '@morgan-stanley/needle';

getRootInjector().configuration.metadataMode = 'explicit';

External Resolution Strategy

In certain environments you will want to delegate type construction to an external DI container or custom constructor function. The externalResolutionStrategy is what makes this possible. When you define this strategy all construction will be delegated to that strategy and the internal type resolution strategy will be ignored. If however you want to adopt a fallback strategy, first checking your external resolver then falling back to this injector you can achieve this by returning a special value TYPE_NOT_FOUND which will signal to needle that it should now attempt to resolve the type as the external one couldn't. This mechanism allows developers to completely control type construction so that they can inject their own pipelines into the process.

import { getRootInjector, IInjector, TYPE_NOT_FOUND } from '@morgan-stanley/needle';

const dummyStrategy: IExternalResolutionConfiguration = {
    resolver: (type: any, currentInjector: IInjector, locals?: any[]) => {
        if(type === MyCustomType){
            return new MyCustomType();
        } else {
            return TYPE_NOT_FOUND;
        }
    }
    cacheSyncing: true;
}

getRootInjector().configuration.externalResolutionStrategy = dummyStrategy;

Setting cacheSyncing to true will ensure that our local cache will be updated when each type instance is resolved. This is useful when you are performing bridging between an external container and the local one. Technically tho, if the external DI strategy is implementing caching you will not need to sync the cache and instead re-request to the strategies resolver should result in a cached instance.

Getting lists of registered types

When working with other libraries you may wish to resolve a list of types that have been registered with the container. For this there are three utility methods you can use getRegisteredTypes, getRegisteredTypesWithDependencies and getRegisteredTypesWithFactories.

import { getRegisteredTypes } from '@morgan-stanley/needle';

const types = getRegisteredTypes(); //Returns an array of raw types.

const typesWithDeps = getRegisteredTypesWithDependencies(); //Returns an Array<{provide: any, deps: Array<>any}>

const typesWithFactories = getRegisteredTypesWithFactories(); //Returns an Array<{provide: any, useFactory: () => T)}>

Semantic Injection

Node's module resolution works on a folder hierarchy where an applications dependencies are stored in a node_modules folder and dependencies can either be shared across multiple transient dependencies or localized to a specific dependency's needs. This means that if you have an npm package installed in your app that has a dependency on foo@1.1.1 and another that uses foo@2.0.0, both can co-exist in the same app domain.

This is a powerful feature of the node/npm ecosystem and one that developers take advantage of everyday when building their apps. However, it is often the case that this semantic version isolation is not extended to your DI container. This is something this library is trying help with.

When constructing a tree of dependencies our DI container will guarantee that each injected instance into a constructor will match the semantic version the consuming code was built against. This means that you can introduce new versions of libraries into your application in a more natural and safe manner, avoiding big bang migrations. The DI system will automatically manage the what and where of injection into your types.

Further, due to the way npm organizes semantic versions, if you have two or more dependencies in your app that rely on foo@^1.x.x, then npm will determine what is the latest compatible version of the @foo dependency being used and then synchronize all the others to use that by de-duping out older versions. So versions 1.1.1 and 1.2.1 would be aligned to 1.5.0 if that was being used. Read more about that here

Semantic injection is a powerful technique for isolating change and instead letting it trickle through your system. It can extend all the way through your package hierarchy and requires little effort from the developers to manage.

Integrating with Angular 2+

If you want to integrate this library with Angular's dependency injection system it's a pretty easy thing to do. In the main.ts file of your Angular app you can resolve all the registered providers and then pass them to the platformBrowserDynamic call.

import 'reflect-metadata';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { getRegisteredTypesWithFactories } from '@morgan-stanley/needle';
import { AppModule } from 'app/app.module';

const providers = getRegisteredTypesWithFactories();

platformBrowserDynamic(providers)
    .bootstrapModule(AppModule)
    .catch((err) => console.error(err));

Using Needle's decorators within Angular components

It is important to note that the decorators provided by Needle cannot be used directly within the constructor of an Angular component. Angular is not aware of these decorators so it is unable to resolve the required instances at runtime. When working with Angular components the Injector API should be used instead. To access the Injector API inject an instance of the Injector directly into your component.

import { Injector } from '@morgan-stanley/needle';

@Component({
    selector: 'my-component',
    templateUrl: './my-component.html',
    styleUrls: ['./my-component.scss'],
})
export class MyComponent {
    constructor(injector: Injector) {
        // Example resolutions
        const strategies = injector.getStrategies('work-strategies');
        const carLazy = injector.getLazy(Car);
    }
}

Keywords

injection

FAQs

Package last updated on 24 Jul 2025

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

About

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.

  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc

U.S. Patent No. 12,346,443 & 12,314,394. Other pending.