Discover tsyringe

tsyringe

Constructor dependency injection for TypeScript, in a container small enough to read in an afternoon.

tsyringe is a lightweight dependency injection container for TypeScript and JavaScript. Instead of wiring classes together by hand with new calls scattered through the codebase, you describe what each class needs in its constructor, register the implementations once, and let the container assemble the object graph. It reads constructor types from the metadata TypeScript emits, so most classes need a single decorator. This site collects clear explanations, setup notes, lifecycle behaviour, usage patterns and troubleshooting help for developers adopting tsyringe or trying to understand how it fits an existing project.

npm i tsyringereflect-metadataTypeScript decoratorsOpen source by Microsoft
container.ts
// composition root
import "reflect-metadata";
import { container, injectable, inject } from "tsyringe";

@injectable()
class UserService {
  constructor(
    @inject("Logger") private log: Logger,
    private repo: UserRepository
  ) {}
}

container.register("Logger", { useClass: ConsoleLogger });
const users = container.resolve(UserService);
TokenRegistryProviderInstance

4 lifecycles

Transient, singleton, resolution‑scoped and container‑scoped.

Test friendly

Child containers swap real services for fakes in seconds.

Easy to Understand

Plain-language guides that explain dependency injection before they explain the API, so the concepts stick.

Feature Focused

Lifecycles, providers, tokens and child containers, each covered with the detail a real project needs.

Organised Resources

Setup notes, usage steps and troubleshooting grouped so you can jump straight to the part you need.

Developer Friendly

Practical patterns and short examples aimed at people writing code today, not abstract theory.

The basics

What Is tsyringe?

tsyringe is a small dependency injection container for TypeScript and JavaScript, published by Microsoft as the tsyringe package on npm. It gives an application one place to describe how objects are created: which implementation satisfies which dependency, and how long each instance should live, instead of spreading that knowledge across constructors and import statements.

The core idea is constructor injection. A class declares what it needs as constructor parameters and is marked with @injectable(). When you ask the container for it using container.resolve(UserService), tsyringe inspects the constructor parameter types, resolves each one in turn, and hands back a fully built instance. Because the class never constructs its own collaborators, a real implementation can be replaced with a fake during testing without editing the class.

TypeScript interfaces do not exist at runtime, so anything that is not a concrete class is injected through a token: a string, a symbol, or a class used as a key. You pair @inject("Logger") on the parameter with a matching registration such as container.register("Logger", { useClass: ConsoleLogger }) during start-up, and the container connects the two.

Registrations support several provider shapes, including classes, ready-made values, factory functions and aliases pointing at other tokens. Lifecycles then decide whether a resolution produces a fresh object or reuses an existing one. Child containers let a subsystem or a test suite override selected registrations while inheriting everything else from the parent.

Metadata matters

tsyringe reads constructor types from the metadata TypeScript emits when emitDecoratorMetadata is enabled, which is why a Reflect metadata polyfill such as reflect-metadata has to be imported once, before any decorated class loads.

tsyringe at a glance

  • Package name: tsyringe (npm)
  • Language: TypeScript, and JavaScript with manual registration
  • Injection style: Constructor injection via decorators
  • Runtime: Node.js and browser bundles
  • Prerequisite: A Reflect metadata polyfill
  • Lifecycles: Transient, singleton, resolution & container scoped
  • Extras: Child containers, factories, disposables

tsyringe.com is an independent educational resource. It is not affiliated with, endorsed by or operated by Microsoft, and it does not host or distribute the package.

Why teams reach for it

  • Removes hand-written wiring from feature code
  • Makes dependencies explicit and reviewable
  • Swaps implementations without editing consumers
  • Keeps object lifetimes in one visible place
Capabilities

tsyringe Features

Six capabilities that cover most of what a project needs from a dependency injection container, from the first decorator to the last teardown.

Constructor injection

Decorate a class with @injectable() and list its collaborators as constructor parameters. The container reads those parameter types, resolves each dependency and returns a finished instance, so no class has to know how its own dependencies are built.

Token-based registration

Interfaces disappear at compile time, so tsyringe injects them through tokens. A string, symbol or class acts as the key, @inject() marks the parameter, and the registration decides which concrete implementation that key resolves to.

Four lifecycles

Transient returns a new instance every time, singleton shares one for the life of the container, resolution-scoped shares within a single resolve call, and container-scoped gives each child container its own instance.

Flexible providers

Register a class, a ready-made value, a factory function that builds the object itself, or a token that forwards to another token. Factory helpers cover common cases such as caching an instance per container.

Child containers

createChildContainer() produces a container that inherits every parent registration but can override selected ones. It is the usual way to isolate a request, a feature module or a single test from global state.

Awkward cases handled

delay() defers construction so two classes can depend on each other, @injectAll() collects every implementation registered against a token, and disposable instances can be cleaned up when the container is disposed.

The flow

How tsyringe Works

Four steps take a project from plain classes to a container that assembles the whole object graph for you.
01

Enable the metadata

Install the package alongside a Reflect metadata polyfill, switch on experimentalDecorators and emitDecoratorMetadata, then import the polyfill once at the very top of your entry file.

02

Describe the class

Add @injectable() to a class and let its constructor list what it depends on. Use @inject(token) for anything that is an interface, a primitive or a value the compiler cannot describe on its own.

03

Register the answers

In one composition root, tell the container which provider satisfies each token: a class, a value, a factory or another token. Choose the lifecycle here so object lifetimes stay visible in a single file.

04

Resolve and run

Call container.resolve() once at the application entry point. tsyringe walks the dependency graph, builds everything in order, and returns the finished object ready to use.

Search intent

Why People Search for tsyringe

Dependency injection is one of those topics where the idea is simple but the first hour is frustrating. Nearly every question about tsyringe falls into one of a handful of buckets, and knowing which bucket you are in usually points straight at the fix.

The six themes here shape the rest of this page. Setup and compatibility answer the configuration questions, the features and lifecycle sections deal with behaviour, and the troubleshooting section covers the errors that appear most often once a project is running.

Start here if you are stuck

  • Nothing resolves at all → check the polyfill import
  • One class fails → check its decorator and tokens
  • Wrong instance count → check the lifecycle
  • Works in tests only → check container isolation

Understanding the concept

Most visitors arrive wanting a straight answer to what dependency injection actually buys them, and how a container differs from simply calling new in the right order.

Configuration problems

Decorator and metadata settings are the most common stumbling block. Compiler flags, the polyfill import order and bundler behaviour all have to line up before the first resolve succeeds.

Interface injection

Developers coming from C# or Java expect to inject an interface directly. They search to find out why that cannot work in TypeScript and what token pattern replaces it.

Lifetime confusion

Questions about why a supposedly shared service produced two instances, or why state leaked between tests, usually come down to a lifecycle or container choice.

Testing patterns

Replacing a dependency with a stub is one of the main reasons to adopt a container, so people look for the cleanest way to do it without polluting the global container.

Comparisons and fit

Before committing, teams want to know how tsyringe compares with manual wiring or larger frameworks, and whether a small container is enough for their codebase.

Environments

tsyringe Compatibility

Where the container runs, what it expects from your toolchain, and where the answer genuinely depends on the version you install.

Node.js

Runs in ordinary Node.js server and CLI projects. The package ships builds that work with both CommonJS and ES module output; pick whichever matches your tsconfig module setting.

TypeScript

The intended home. You need experimentalDecorators and emitDecoratorMetadata turned on. Decorator support has changed across TypeScript releases, so confirm the requirements for the version you install.

JavaScript

Usable without TypeScript, but plain JavaScript emits no parameter metadata. You register factories or declare every dependency explicitly instead of relying on automatic constructor inspection.

Browsers and bundlers

Works in front-end builds through webpack, Vite, Rollup and similar tools. The metadata polyfill has to be bundled and evaluated before any decorated module is imported.

Frameworks

Framework-agnostic by design. It sits underneath Express, Fastify, React, Angular or anything else, because it only cares about classes and tokens, not about your HTTP or view layer.

Version differences

Requirements are not identical across releases: supported TypeScript versions, decorator behaviour and build targets have all moved over time. Check the release notes for the exact version you install.

No invented compatibility claims. Support depends on the tsyringe version, your TypeScript version, your build tooling and your chosen Reflect polyfill. Where those differ, behaviour differs too, so treat the official repository and release notes as the authority for the version you actually install.

Practical steps

How to Use tsyringe

A working order for adding the container to a project, from installation to the first resolved object graph.
01

Add the package and polyfill

Install tsyringe and a Reflect metadata implementation such as reflect-metadata in the same step, so the runtime requirement is never forgotten later.

02

Turn on decorator metadata

In tsconfig.json, set experimentalDecorators and emitDecoratorMetadata to true. Without the second flag the container cannot see constructor parameter types.

03

Import the polyfill first

Put import "reflect-metadata"; on the first line of your entry file. It must run before any decorated class is evaluated, which usually means before every other import.

04

Decorate and declare

Mark injectable classes with @injectable(), or @singleton() when a single shared instance is wanted. Add @inject(token) to parameters the compiler cannot describe.

05

Register in one place

Keep registrations in a composition root that runs at start-up. Group them by feature and choose the lifecycle deliberately rather than defaulting everything to singleton.

06

Resolve at the entry point

Resolve the top-level object once and let the container build the rest. Avoid passing the container into ordinary classes, which turns dependency injection back into a service locator.

Setup

How to Install tsyringe

What to check before installing, what to change in your build, and how to confirm the container is wired correctly.

Requirements

  • A Node.js version supported by your build tooling
  • npm, yarn or pnpm for package installation
  • TypeScript with decorator support, for the decorator API
  • A Reflect metadata implementation available at runtime

Preparation

Enable experimentalDecorators and emitDecoratorMetadata in tsconfig.json. If you compile with Babel instead of tsc, you will need the equivalent legacy-decorator and TypeScript metadata plugins, because Babel does not emit that metadata on its own.

Installation

Install the container and the polyfill together, for example npm install tsyringe reflect-metadata. Alternative Reflect implementations exist and can be used instead, but only one should be loaded in a build.

Configuration

Import the polyfill once, on the first line of the application entry point, then create your composition root. Register every token there and keep that file free of business logic so the wiring stays easy to scan.

Verification

Resolve one simple class and log the result. If it constructs without throwing a metadata error, the compiler flags, polyfill and decorators are all in place. Add a small test that resolves the same class to catch regressions.

Updating

Read the release notes before upgrading. Decorator and TypeScript requirements have changed across versions, so update in a branch, run the test suite, and check that your bundler still evaluates the polyfill first.

Install from the source you trust

This page does not host files or link to mirrors. Install the package from your usual registry, or build from the official repository, and confirm the version you pulled matches the documentation you are reading. Version numbers, requirements and decorator behaviour should always be checked against the release you actually installed.

In practice

What Good tsyringe Code Looks Like

The same three habits show up in almost every project that stays happy with a container after the first month.
Composition root

One file decides how the whole application is built

Every provider shape lives side by side in the same place: a class for the normal case, a ready-made value for configuration, a factory when construction needs logic, and a singleton registration when one shared instance is the point. Nothing else in the codebase has to know which implementation won.

// container.ts — the only file that knows how things are built
container.register(TOKENS.Logger,  { useClass: ConsoleLogger });
container.register(TOKENS.Clock,   { useValue: systemClock });
container.register(TOKENS.Mailer,  { useFactory: buildMailer });
container.registerSingleton(UserRepository);

export const app = () => container.resolve(Application);
  • Wiring changes stay in one reviewable diff
  • New developers can read the graph without grepping
  • Environment differences become one swapped registration

Pick the lifetime

Transient builds fresh, singleton shares one, resolution scope shares inside a single resolve, and container scope gives each child its own.

Tokens instead of guesswork

TOKENS.LoggerTOKENS.ClockTOKENS.MailerTOKENS.Config

Export tokens as constants or symbols and the compiler catches the typo that a loose string literal would hide until runtime.

Fakes without ceremony

A child container overrides one registration for a single test and leaves the parent untouched, so suites stop leaking state into each other.

Side by side

tsyringe Comparison

An honest look at what a container changes compared with wiring dependencies by hand. Neither column is the right answer for every project.

Feature

tsyringe

Manual wiring

Notes

Building objects

The container resolves constructors from decorators and registrations.

Every dependency is constructed by hand, in the right order, at each call site.

Both work. The difference grows with the size of the graph.

Swapping an implementation

Change one registration in the composition root.

Edit each place the class is constructed.

Most visible in tests and multi-environment builds.

Object lifetimes

Declared per registration using four defined lifecycles.

Handled with module-level variables and team conventions.

Declared lifetimes are easier to audit than conventions.

Interfaces

Injected through a string, symbol or class token.

Passed directly as a typed constructor argument.

Manual wiring needs no tokens, which is genuinely simpler.

Set-up cost

Compiler flags, a metadata polyfill and decorators.

None at all.

The main trade-off to weigh before adopting a container.

When mistakes surface

A missing registration is reported when the token is resolved.

A missing argument is reported by the compiler.

Type checking catches more wiring mistakes earlier.

Balanced view

Highlights and Trade-offs

Worth reading before you commit a codebase to a container.

tsyringe Highlights

  • Small API surface. A handful of decorators and container methods covers most projects.
  • Explicit dependencies. Constructors document what a class needs, which makes reviews easier.
  • Deliberate lifetimes. Four lifecycles put object lifetime in the registration instead of in convention.
  • Test isolation. Child containers override single registrations without touching global state.
  • Framework agnostic. Nothing about it assumes a particular HTTP, UI or database layer.
  • Escape hatches. Factories, deferred resolution and disposables handle the awkward cases.

Things to Consider

  • Build configuration is required. Decorator flags and a metadata polyfill must be in place before anything resolves.
  • Interfaces need tokens. Injecting an interface directly is impossible, so a token layer is unavoidable.
  • Some errors move to runtime. A missing registration surfaces when it is resolved, not when you compile.
  • Decorators keep evolving. TypeScript’s decorator story has changed over time, so pin versions and read release notes.
  • Easy to overuse. A short script or a single-purpose tool rarely benefits from a container.
  • Container misuse. Passing the container around turns injection into a service locator and hides dependencies again.
Fixes

Common tsyringe Problems & Solutions

Eight failures that account for most of the time lost with a decorator-based container, each with the reason behind it.

Nothing resolves and the error mentions metadata

Likely reason: The Reflect metadata polyfill was never imported, or it loaded after a decorated class.

Try this: Move import "reflect-metadata"; to the first line of the entry file, above every other import, and make sure exactly one polyfill is present in the bundle.

Type information is not known for a class

Likely reason: The class is missing @injectable(), or emitDecoratorMetadata is off.

Try this: Add the decorator, confirm both decorator flags in tsconfig.json, delete stale build output and compile again so fresh metadata is emitted.

An interface cannot be injected

Likely reason: Interfaces and type aliases are erased during compilation, so nothing exists at runtime to resolve.

Try this: Introduce a token, mark the parameter with @inject(TOKEN), and register the concrete class against that token in the composition root.

Two classes depend on each other

Likely reason: A circular reference means one class is still undefined while the other is being constructed.

Try this: Wrap the reference with delay() so resolution is deferred, and consider extracting the shared behaviour into a third class that both can depend on.

A shared service is built more than once

Likely reason: The registration is transient, or the class is being resolved from a different container.

Try this: Register it with a singleton lifecycle and check which container each call site uses, since a child container may hold its own instance.

State leaks between tests

Likely reason: Every test shares the global container, so instances registered in one test survive into the next.

Try this: Create a child container per test, or clear instances and registrations in your setup hook so each test starts from a known state.

Registrations vanish in a production build

Likely reason: The module containing registrations was never imported, so the bundler removed it as dead code.

Try this: Import the registration module explicitly from the composition root, and verify it survives in the output bundle before shipping.

Decorators break after upgrading TypeScript

Likely reason: Decorator semantics differ between the experimental proposal and newer language versions.

Try this: Keep experimentalDecorators enabled, check the tsyringe release notes for the versions you use, and upgrade the compiler and container in a branch first.

Habits

tsyringe Tips & Best Practices

Six habits that keep a container helpful as the codebase grows.

Keep one composition root

Registrations belong in a single file that runs at start-up. When wiring is scattered across modules, nobody can answer which implementation is live without reading all of them.

Export tokens as constants

A loose string literal is a typo waiting to happen. Export symbols or a frozen token object so the compiler flags a mistake instead of the container failing at runtime.

Never inject the container

Passing the container into a class hides its real dependencies and undoes most of the benefit. Ask for the collaborators themselves and let the container stay at the edge.

Default to the narrowest lifetime

Reach for transient first and promote to singleton only when sharing is genuinely intended. Wide lifetimes are where surprising cross-request state usually comes from.

Isolate every test

Give each test its own child container, or reset instances between runs. Shared containers make failures depend on test order, which is a painful thing to debug later.

Plan for teardown

Anything holding a socket, file handle or timer needs a shutdown path. Implement disposal on those classes and dispose the container when the process stops.

Deep dive

Complete tsyringe Guide

Everything above, expanded into the reasoning behind each decision you will make while adopting the container.

Who tsyringe is for

The container earns its keep once a project has more than a handful of collaborating classes and a real reason to swap one of them. Layered Node services, CLI tools with interchangeable adapters, and front-end applications with a service layer behind the UI are all comfortable fits. A two-hundred-line script is not. If nothing needs replacing and no object has an interesting lifetime, a container adds build configuration without paying it back, and plain constructor arguments remain the clearer choice.

The mental model

Picture the container as a dictionary that maps tokens to instructions. A token identifies something; a provider describes how to produce it. Resolving walks that dictionary: for each constructor parameter the container finds a token, looks up the instruction, and recurses until it reaches a class with no dependencies of its own. Almost every failure is one of three things — the token was never registered, the parameter type could not be read, or the graph contains a cycle. Holding that model makes the error messages far easier to interpret.

Choosing a lifecycle

Transient is the default and the safest: a new instance for every resolution. Singleton keeps one instance for the life of the container, which suits caches, connection pools and configuration objects. Resolution scope shares an instance within a single resolve call, so several classes in the same graph can see the same unit of work. Container scope gives every child container its own copy, which is how per-request state is usually modelled. Pick the narrowest option that fits, and treat any singleton holding mutable state as a decision worth documenting.

Providers in practice

Class providers cover the ordinary case. Value providers hold configuration, clocks, or an SDK client you did not create. Factory providers are for construction that needs a decision, such as reading an environment variable or choosing between two adapters, and helper factories exist for common caching patterns. Token providers alias one key to another, which is useful while a name is being migrated. Keep factories short: once a factory grows branching logic, that logic usually belongs in a class of its own.

Where the container belongs

Only the entry point should call resolve. Everything underneath receives what it needs through its constructor. In an HTTP service that normally means resolving the application object once at boot and creating one child container per request where request-scoped state exists. Frameworks that construct their own objects need a thin adapter that asks the container on their behalf; keeping that adapter small is what stops container references from spreading into ordinary business code.

Keeping upgrades boring

Pin versions, upgrade one thing at a time, and run the suite in between. Because the container depends on decorator and metadata behaviour, a TypeScript, bundler or polyfill upgrade can affect it just as much as upgrading tsyringe itself. Add one smoke test that resolves the top-level object: it fails loudly the moment the metadata pipeline breaks, which is far easier to diagnose than a strange runtime error surfacing from a deployed service days later.

Quick recap

The short version

  • Decorate classes, register tokens, resolve once
  • Interfaces always travel as tokens
  • Lifetimes are a decision, not an accident
  • Child containers isolate tests and requests
  • The polyfill has to load first, every time

Important

Details such as supported TypeScript versions, decorator behaviour and available helpers belong to a specific release. Always confirm them against the documentation for the version installed in your project rather than assuming a page like this one matches it exactly.

Vocabulary

  • Token: the key a dependency is registered under
  • Provider: the instruction for producing a value
  • Lifecycle: how long an instance is reused
  • Composition root: where registration happens
  • Child container: a scoped copy that can override
Where to go next

tsyringe Download & Resources

Placeholder links you can repoint inside Elementor, plus a note on checking what you install.

tsyringe Resources

Reference material covering the container API, decorators and lifecycles. Replace this link with the documentation source your team standardises on.

Installation Guide

The setup path in order: package installation, compiler flags, polyfill import and a first verified resolve. Point this button at your internal onboarding notes if you keep them.

Latest Information

Release notes and version-specific requirements change more often than concepts do. Link this to the changelog for the release your project depends on.

Use reputable sources

No download URLs are supplied on this page, and none should be invented. Install the package through your package manager from a registry you trust, or build it from the official repository. Before relying on any guide, including this one, check that the version information matches the release in your project, since requirements and available helpers differ between versions. Every button in this section is an ordinary Elementor link and can be pointed at your own resources.

Answers

tsyringe Frequently Asked Questions

Twenty questions covering setup, behaviour, versions and the problems that come up most often.

tsyringe is a lightweight dependency injection container for TypeScript and JavaScript, published by Microsoft on npm. It lets classes declare their dependencies as constructor parameters and then builds those objects for you, so wiring lives in one place instead of being spread through the codebase.

You mark a class with a decorator, register the implementations that satisfy each token, and ask the container to resolve the class you need. tsyringe reads constructor parameter types from the metadata TypeScript emits, resolves each dependency in turn, and returns a fully constructed object.

Constructor injection through decorators, token-based registration for interfaces, four lifecycles, several provider types including classes, values, factories and token aliases, child containers for scoped overrides, plus helpers for circular dependencies, collections of implementations and disposal.

The API itself is small, and most classes need one decorator. The difficult part is usually the initial build configuration: decorator flags, the metadata polyfill and import order all have to be right before the first resolve succeeds. After that, day-to-day use is straightforward.

It runs under Node.js and in browser bundles produced by common tools such as webpack, Vite or Rollup. It is framework agnostic, so it can sit underneath a server framework or a front-end application, as long as a Reflect metadata implementation is available at runtime.

Install the package with your package manager alongside a Reflect metadata polyfill, enable the experimental decorator and decorator metadata compiler options, then import the polyfill once at the top of your entry file. After that, create a composition root where registrations live.

Update it like any other dependency, but read the release notes first. Because behaviour depends on decorators and metadata, upgrade in a branch, run the full test suite, and confirm your bundler still evaluates the polyfill before any decorated module loads.

The most common causes are a missing or late polyfill import, decorator metadata not being emitted, a class without its decorator, or a token that was never registered. Reading the exact error message usually points at which of those four applies.

Check that your compiler or build pipeline can emit decorator metadata, that you can control import order at the entry point, and that only one Reflect implementation will end up in the bundle. Also confirm the tsyringe version’s requirements against your TypeScript version.

Yes. Supported TypeScript versions, decorator handling, available helpers and build targets have all changed across releases. Treat the documentation and changelog for the exact version installed in your project as the authority rather than any general summary.

Start with the official repository and its documentation for API details, then supplement with your team’s own onboarding notes. This site organises explanations, setup steps and troubleshooting, but it is a study aid rather than a replacement for the source documentation.

Work from the outside in. Confirm the polyfill loads first, then that the class carries a decorator, then that every token it needs has a registration, and finally that you are resolving from the container you think you are. Most problems fall out at one of those four checks.

Yes, though the work is proportional to how widely it was used. Replace resolutions with manual construction starting at the entry point, remove the decorators, then drop the package and the metadata compiler options if nothing else in the project depends on them.

Commit your current state, note the versions of tsyringe, TypeScript and your polyfill, and make sure the test suite passes beforehand. Having a smoke test that resolves your top-level object makes any regression obvious immediately after the upgrade.

It requires a Reflect metadata implementation at runtime, which is a separate package. Using the decorator API also assumes a compiler that emits decorator metadata, which in practice means TypeScript or an equivalently configured Babel pipeline.

Look at the dependency entry in your package.json, or ask your package manager to list the installed package. The lockfile shows the exact resolved version, which is the number worth quoting when comparing behaviour against documentation.

Metadata errors from a late polyfill import, missing decorators, attempts to inject an interface directly, circular dependencies, unexpected duplicate instances caused by lifecycle choices, and test pollution from a shared global container are the ones that come up most often.

You can use the container from JavaScript, but plain JavaScript emits no parameter metadata, so automatic constructor inspection is unavailable. In that case dependencies are registered explicitly, typically through factory providers, which is more verbose but perfectly workable.

No. This site is an independent educational resource about the tsyringe container. It is not affiliated with, endorsed by or operated by Microsoft, it does not host the package, and it does not speak for the project maintainers.

Get the build configuration right before writing much code, keep registrations in one composition root, resolve only at the entry point, and choose lifecycles deliberately. Those four habits prevent most of the confusion that new users run into during the first week.

Explore tsyringe

Read the guide from the top, or jump straight to installation and troubleshooting. Everything on this page is written to be useful while you have an editor open next to it.

Independent educational resource · not affiliated with Microsoft

Scroll to Top