Introducing Socket Firewall: Free, Proactive Protection for Your Software Supply Chain.Learn More
Socket
Book a DemoInstallSign in
Socket

@gitlab/needle

Package Overview
Dependencies
Maintainers
7
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@gitlab/needle

`@gitlab/needle` is a lightweight dependency injection framework for TypeScript, using ES stage 3 decorators. It provides a type-safe solution for managing dependencies in your TypeScript projects.

latest
npmnpm
Version
1.5.0
Version published
Weekly downloads
2.7K
3.88%
Maintainers
7
Weekly downloads
 
Created
Source

@gitlab/needle documentation

@gitlab/needle is a lightweight dependency injection framework for TypeScript, using ES stage 3 decorators. It provides a type-safe solution for managing dependencies in your TypeScript projects.

Key features

  • Implements the new ES stage 3 decorators standard (the TS experimental decorators are now marked as legacy and even though still supported, it's recommended to use the standard going forward)
  • Compatible with the latest TypeScript and esbuild without special build flags
  • Fully type-safe, ensuring correct dependency types and constructor arguments

Basic usage

Define interfaces

interface A {
  field: string;
}

interface B {
  hello(): string;
}

Create interface IDs

import { createInterfaceId } from '@gitlab/needle';

const A = createInterfaceId<A>('A');
const B = createInterfaceId<B>('B');

Implement and decorate classes

@gitlab/needle provides multiple decorator options for defining services that can be registered with the container.

@Implements

The @Implements decorator identifier which interface(s) a class implements.

import { Implements } from '@gitlab/needle';

@Implements(A)
class AImpl implements A {}

@Service

The @Service decorator defines a service and is required by the system to create a service descriptor. We can specify dependencies and lifetime in this decorator.

import { Service, ServiceLifetime } from '@gitlab/needle';

@Service({
  dependencies: [B, C],
  lifetime: ServiceLifetime.Singleton,
  autoActivate: false
})
class AImpl implements A {
  constructor(b: B, c: C) {}
}

The lifetime parameter can be one of:

  • ServiceLifetime.Singleton - The service is created once and reused for all requests.
  • ServiceLifetime.Transient - A new instance of the service is created for each time the service is requested.
  • ServiceLifetime.Scoped - A new instance is created for each scope

The autoActivate parameter controls whether the service should be resolved automatically when the container is built. This is useful for services that act as an entry point, for example, initialization of a websocket listener.

@Injectable

The @Injectable decorator is a convenience decorator that combines the functionality of @Implements and @Service. By default, it declares the service to have a lifetime of Singleton

import { Injectable } from '@gitlab/needle';

@Injectable(A, [])
class AImpl implements A {
  field = 'value';
}

@Injectable(B, [A])
class BImpl implements B {
  #a: A;

  constructor(a: A) {
    this.#a = a;
  }

  hello() {
    return `B.hello says A.field = ${this.#a.field}`;
  }
}

Register Services with ServiceCollection

Purpose

ServiceCollection is responsible for:

  • Registering services and their dependencies
  • Validating service registration
  • Building a ServiceProvider that can resolve those services

Usage

Creating a ServiceCollection
import { ServiceCollection } from '@gitlab/needle';

const services = new ServiceCollection();
Registering Services

ServiceCollection provides several ways to register services:

Method 1: Adding Classes Decorated with @Service or @Injectable
import { ServiceCollection, Injectable, createInterfaceId } from '@gitlab/needle';

// Define interface and ID
interface Logger {
  log(message: string): void;
}
const Logger = createInterfaceId<Logger>('Logger');

// Implement and decorate class
@Injectable(Logger, [])
class ConsoleLogger implements Logger {
  log(message: string) {
    console.log(message);
  }
}

// Register with ServiceCollection
const services = new ServiceCollection();
services.addClass(ConsoleLogger);
Method 2: Adding Service Descriptors Directly

For more advanced scenarios, you can create and add service descriptors:

import {
  createInstanceDescriptor,
  createConstructorDescriptor,
  ServiceLifetime
} from '@gitlab/needle';

// Add an existing instance
const config = new ConfigurationService();
services.add(
  createInstanceDescriptor({
    instance: config,
    aliases: [Configuration]
  })
);

// Add a constructor-based service
services.add(
  createConstructorDescriptor({
    implementation: DatabaseService,
    aliases: [Database],
    dependencies: [Configuration],
    lifetime: ServiceLifetime.Scoped,
    autoActivate: false
  })
);
Chaining Registrations

Registration methods return the ServiceCollection instance to support method chaining:

services
  .addClass(UserService)
  .addClass(AuthenticationService)
  .add(
    createInstanceDescriptor({
      instance: new ConfigurationService(),
      aliases: [Configuration],
    }),
  );

Validating the ServiceCollection

Before building a ServiceProvider, you can validate the service registrations to ensure there are no issues:

const validationResult = services.validate();

if (!validationResult.isValid) {
  console.error('Service validation failed:');
  validationResult.errors.forEach((error) => console.error(error.message));
}

The validation checks for:

  • Missing dependencies
  • Circular dependencies
  • Services with multiple implementations where a single implementation is required

Building a ServiceProvider

After registering all services, you can build a ServiceProvider to start resolving services:

// This will validate services and throw an error if validation fails
const serviceProvider = services.build();

If you've set autoActivate: true for any services, they will be automatically instantiated when the provider is built.

ServiceProvider

Purpose

ServiceProvider is responsible for:

  • Resolving service identifier based on registered service descriptors
  • Managing service lifetime (singleton, transient, scoped)
  • Resolving dependencies between services

Using ServiceProvider

Getting a Service

To get a single instance of a service:

const logger = serviceProvider.getRequiredService(Logger);
logger.log('Hello, world!');

This method will:

  • Throw ServiceNotRegisteredError if no implementation is registered
  • Throw AmbiguousMatchError if multiple implementations are registered
Getting All Services for an Interface

To get all registered implementations of a service:

const loggers = serviceProvider.getServices(Logger);
loggers.forEach((logger) => logger.log('Hello from all loggers'));

This method does not throw and returns an empty array if no services are registered for the interface.

Managing Service Scopes

Understanding Service Lifetimes

The behavior of services depends on their registered lifetime:

  • Singleton: One instance for the entire application lifetime
  • Scoped: One instance per scope
  • Transient: New instance every time the service is requested

Creating and Using Scopes

copes provide isolation for services marked with ServiceLifetime.Scoped. They are useful for request-scoped services or isolating services whose lifetime is distinct from the application lifetime.

// Root provider from ServiceCollection
const rootProvider = services.build();

// Create a scope
const scope = rootProvider.createScope();

try {
  // Get a scoped service within this scope
  const userService = scope.getRequiredService(UserService);

  // Use the service...
  await userService.processRequest();
} finally {
  // Always dispose the scope when finished to prevent memory leaks
  scope.dispose();
}
Nested Scopes

You can create nested scopes to establish hierarchy in your service resolution:

const rootProvider = services.build();
const parentScope = rootProvider.createScope();

try {
  // Create a child scope
  const childScope = parentScope.createScope();

  try {
    // Use services from the child scope
    const service = childScope.getRequiredService(ScopedService);
    // ...
  } finally {
    childScope.dispose();
  }
} finally {
  parentScope.dispose();
}
Resource Disposal

Properly disposing scopes is important to prevent memory leaks, especially for scoped services that implement the Disposable interface:

interface Disposable {
  dispose(): void;
}

@Implements(DataService)
@Service({
  dependencies: [],
  lifetime: ServiceLifetime.Scoped
})
class DatabaseConnection implements DataService, Disposable {
  #connection: Connection;

  constructor() {
    this.#connection = createConnection();
  }

  // Methods...

  dispose() {
    // Clean up resources when the scope is disposed
    this.#connection.close();
  }
}

// Usage
const scope = rootProvider.createScope();
try {
  const db = scope.getRequiredService(DataService);
  // Use database...
} finally {
  // This will call dispose() on all scoped services in this scope
  scope.dispose();
}

Initialize container (DEPRECATED)

import { Container } from '@gitlab/needle';

const container = new Container();
container.instantiate(AImpl, BImpl);

Retrieve dependencies (if needed) (DEPRECATED)

const b = container.get(B);
console.log(b.hello());

Advanced usage

Add external dependencies

For dependencies that can't be provided by the container, you need to add them manually. Here's an example of adding an external logger dependency:

import { createInterfaceId, brandInstance } from '@gitlab/needle';

interface Logger {
  log(...args: unknown[]): void;
}

const Logger = createInterfaceId<Logger>('Logger');

const customLogger: Logger = {
  log: (...args) => console.log('custom logger', ...args),
};

container.addInstances(brandInstance(Logger, customLogger));

Example

We had to add the Language Server connection to the container (we get it from the VS Code framework already created). For that we created an alias for the Connection called LsConnection (so we can create the InterfaceId) and added it to the container.

Collections

When you need to inject all registered instances of a particular interface, you can use collection dependencies:

import { createInterfaceId, collection } from '@gitlab/needle';

// Define interface and create its ID
interface Plugin {
  execute(): void;
}
const Plugin = createInterfaceId<Plugin>('Plugin');

// Implement multiple plugins
@Injectable(Plugin, [])
class Plugin1 implements Plugin {
  execute() {
    console.log('Plugin 1');
  }
}

@Injectable(Plugin, [])
class Plugin2 implements Plugin {
  execute() {
    console.log('Plugin 2');
  }
}

// Inject all plugins into a manager
@Injectable(PluginManager, [collection(Plugin)])
class PluginManager {
  constructor(private plugins: Plugin[]) {}

  executeAll() {
    this.plugins.forEach((p) => p.execute());
  }
}

// Initialize
const container = new Container();
container.instantiate(Plugin1, Plugin2, PluginManager);

// Use
const manager = container.get(PluginManager);
manager.executeAll(); // Outputs: "Plugin 1" "Plugin 2"

If the same collection is required in multiple places, a collectionId variable can be created:

import { createInterfaceId, createCollectionId } from '@gitlab/needle';

// Same Plugin setup as example above

const PluginsCollection = createCollectionId(Plugin);

@Injectable(PluginManager1, [PluginsCollection])
class PluginManager1 {
  constructor(private plugins: Plugin[]) {}
}

@Injectable(PluginManager2, [PluginsCollection])
class PluginManager2 {
  constructor(private plugins: Plugin[]) {}
}

API reference

createInterfaceId<T>(id: string): InterfaceId<T>

Creates a unique runtime identifier for an interface.

createCollectionId<T>(interfaceId: InterfaceId<T>): CollectionId<T>

Creates a runtime identifier for a collection of interfaces.

collection<T>(interfaceId: InterfaceId<T>): CollectionId<T>

Alias of createCollectionId, intended for use directly within @Injectable when no intermediate variable is required.

@Injectable(id: InterfaceId<I>, dependencies: (InterfaceId<unknown> | CollectionId<unknown>)[])

Decorator for classes to mark them as injectable and specify their dependencies.

Container

  • instantiate(...classes: Class[]): void: Initializes the specified classes.
  • addInstances(...instances: BrandedInstance<object>[]): void: Adds pre-initialized objects to the container.
  • get<T>(id: InterfaceId<T>): T: Retrieves an instance from the container.

brandInstance<T>(id: InterfaceId<T>, instance: T): BrandedInstance<T>

Brands an instance for use with container.addInstances().

Best Practices

  • Define interfaces for your dependencies.
  • Use createInterfaceId for each interface.
  • Implement classes and decorate them with @Injectable.
  • Initialize your container in the application's entry point.
  • Use container.get() sparingly, preferring constructor injection.

Examples in the code

Troubleshooting

  • Ensure all classes are decorated with @Injectable.
  • Ensure your InterfaceId instances have the correct interface type and string identifier.
  • Ensure that all constructor arguments implement interfaces mentioned in the @Injectable decorator.

VS Code snippet

To reduce boilerplate, consider adding this snippet to your VS Code TypeScript snippets:

{
  "New DI Class": {
    "prefix": "diclass",
    "body": [
      "import { Injectable, createInterfaceId } from '@gitlab/needle';",
      "",
      "export interface ${1:InterfaceName} {}",
      "",
      "export const ${1:InterfaceName} = createInterfaceId<${1:InterfaceName}>('${1:InterfaceName}');",
      "",
      "@Injectable(${1:InterfaceName}, [])",
      "export class Default${1:InterfaceName} implements ${1:InterfaceName} {}"
    ],
    "description": "Creates DI framework boilerplate"
  }
}

FAQ

Why didn't you decorate the constructor parameters?

You can't, because the ES Stage 3 decorators don't support decorating method/function parameters.

How is this implemented?

  • the type definition of @Injectable is a dark, write-only type magic
  • the Container implementation is a straightforward graph traversing and string validation

Learn more in the blog post mentioned in the next section.

Compatibility

Current implementation works both for legacy experimental decorators and new TypeScript 5 decorators. If the decorators are used in a legacy environment, you won't get errors explaining that you should only use Implements and Service decorators on classes.

Additional resources

FAQs

Package last updated on 11 Sep 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