TSyringe: Microsoft's Lightweight DI Container for TypeScript
Lightweight dependency injection container for JavaScript/TypeScript
At a glance
- What is it?
- TSyringe is a constructor-injection container for TypeScript and JavaScript built on decorators and reflect-metadata. It stays small, but its decorator pipeline and its last release date shape where it fits.
- Who is it for?
- TSyringe fits TypeScript codebases that already compile with experimentalDecorators and emitDecoratorMetadata, where constructor injection and token-based registration remove manual wiring. Skip it if you cannot use decorators, if you need a container that resolves without reflect-metadata, or if you want a library with frequent releases: the newest entry in the release list is v4.4.0 from 2020-11-09.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 21 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What TSyringe solves, and the codebase it assumes
The README describes TSyringe as "a lightweight dependency injection container for TypeScript/JavaScript for constructor injection." The problem it addresses is ordinary: a class declares the collaborators it needs in its constructor, and the container builds the graph instead of you writing the wiring by hand. The README's own example is a class decorated with @injectable() whose constructor takes a Database, resolved later through container.resolve(Foo).
That design comes with an assumption baked in. The README's installation section tells you to set experimentalDecorators and emitDecoratorMetadata in tsconfig.json, and TSyringe reads constructor parameter types from the metadata those flags emit. Without them, the container has no type information to work from and you fall back to explicit tokens. This is a TypeScript-first library, and the repository layout reflects it: src/, test/, and a typescript/ directory holding the build configs, with package.json pointing main at dist/cjs/index.js and typings at dist/typings/index.d.ts.
Who it is for: teams already using decorators, or willing to adopt them, that want a container small enough to read. The package has one runtime dependency, tslib, and declares engines.node >= 6.0.0. That is a deliberately narrow footprint compared with containers that ship their own reflection layer.
How the container resolves a class: metadata, tokens, registrations
Resolution is metadata-driven. The @injectable() decorator, in the README's words, "allows the class' dependencies to be injected at runtime," and the library "relies on several decorators in order to collect metadata about classes to be instantiated." When you call container.resolve(Foo), the container inspects the constructor parameter metadata, looks up a registration for each parameter type, and constructs the dependencies before constructing Foo.
Interfaces do not exist at runtime, so they cannot appear in that metadata. The README handles this with @inject(), a parameter decorator that "allows for interface and other non-class information to be stored in the constructor's metadata." You supply a string or other token, as in @inject("Database"). Registration then maps that token to a concrete implementation.
Three decorators change the shape of resolution rather than the graph. @singleton() registers the class as a singleton in the global container. @autoInjectable() replaces the decorated class's constructor with a parameterless one whose dependencies are auto-resolved, and the README notes resolution is performed using the global container; because the constructor takes no arguments, the parameters must be optional, written as database?: Database. @injectAll() targets array parameters and fills them from every registration under a token. There are also transform variants, @injectWithTransform() and @injectAllWithTransform(), which pass the resolved object through a transformer before it reaches the constructor. The README's example uses a FeatureFlagsTransformer implementing Transform<FeatureFlags, boolean> to turn a flags object into a single boolean.
The container also supports child containers, interception, clearing instances, and an isRegistered check. Those are container-level features rather than decorator-level ones, and the README documents each as its own subsection.
Installing TSyringe and getting a first resolve working
Installation is two commands plus a compiler change. The README gives npm and yarn forms; the project itself is developed with yarn.
npm install --save tsyringeThen the compiler flags, which are not optional if you want automatic type resolution:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Finally a Reflect polyfill. The README lists reflect-metadata, core-js/es7/reflect, and @abraham/reflection, and states the polyfill import "should only be added once, and before DI is used."
// main.ts
import "reflect-metadata";
// Your code here...With those in place, a minimal class and its resolution look like this, adapted from the README's @injectable() example:
import {injectable, container} from "tsyringe";
@injectable()
class Foo {
constructor(private database: Database) {}
}
const instance = container.resolve(Foo);What you should see: container.resolve(Foo) returns a Foo whose database parameter was itself resolved by the container. If the Database type has no registration, resolution fails rather than silently passing undefined. That is the behaviour worth confirming first, because it is the difference between a misconfiguration you notice immediately and one you find in production.
Babel users, including React Native projects, need an extra step. The README instructs installing babel-plugin-transform-typescript-metadata as a dev dependency and adding it to the Babel config's plugins array, because Babel does not emit TypeScript metadata on its own.
yarn add --dev babel-plugin-transform-typescript-metadataWithout that plugin the decorators still run, but the constructor metadata is absent, so parameter types cannot be inferred.
Circular dependencies, optional injection, and where TSyringe breaks down
The README devotes a whole section to circular dependencies, which is a fair signal of how often they come up. It documents a delay helper function for the case where two classes depend on each other, and a separate subsection for interfaces combined with circular dependencies. If your object graph has cycles, you are working against the container's construction order rather than with it, and the delay helper is the documented escape hatch.
The second sharp edge is optional injection. By default, the README states, @inject() throws an exception if no registration is found. To get undefined instead, you pass { isOptional: true } as the second parameter. @injectAll() behaves the same way and throws when no registrations exist unless you pass the same option, in which case it returns an empty array. Defaulting to a throw is defensible, but it means every optional dependency is an explicit opt-in, and forgetting the flag turns a missing registration into a startup failure.
The third is decorator support itself. The README's own section on Babel exists because plain Babel output carries no TypeScript metadata. If your build pipeline cannot run experimentalDecorators and emitDecoratorMetadata, or cannot add the Babel plugin, TSyringe's automatic resolution does not work and you would be writing tokens for every dependency, at which point the value of the container shrinks considerably.
One more boundary worth naming: the README has a Non goals section, and it does not document rollback or migration behaviour for existing hand-wired code. Adoption is a rewrite of how instances are created, not a drop-in wrapper.
TSyringe vs Inversify: the difference is scope, not syntax
The comparison people search for is TSyringe vs Inversify, and the meaningful difference is how much the container does. TSyringe is constructor injection with a small decorator set: @injectable, @singleton, @autoInjectable, @inject, @injectAll, @scoped, plus the transform variants. Its package.json lists one runtime dependency, tslib. Inversify is a larger framework with its own concepts, and its documentation and API surface are correspondingly wider.
The practical consequence is where you spend configuration. In TSyringe, interface injection goes through @inject("Database") with a string token, and registration maps that token to an implementation. If your graph is mostly classes with a handful of interface seams, that is enough. If you need a container that carries more structure out of the box, TSyringe's non-goals section is the honest place to check whether your requirement is in scope before you start.
There is also a maintenance dimension that has nothing to do with features. The release list for TSyringe shows v4.4.0 dated 2020-11-09, following v4.2.0 and v4.1.0 in May 2020. The package.json version is 4.10.0, and the repository's last push was on 2026-09-08, so the code has moved since that release entry was recorded. Anyone treating release cadence as a proxy for project health should look at both facts rather than one.
Maintenance cost, licence, and what upgrading involves
The licence is MIT, stated in package.json and present as a LICENSE file at the repository root. MIT permits commercial use, modification, and redistribution with the licence and copyright notice retained. That is the standard reading; it is not legal advice, and if your organisation has specific obligations around attribution, check them against the LICENSE file rather than this summary.
Upgrade cost is mostly determined by the decorator pipeline rather than by the container API. The public surface described in the README is stable and small, so the churn risk sits in your build: tsconfig.json flags, the single Reflect polyfill import, and for Babel users the babel-plugin-transform-typescript-metadata entry in the plugins array. A TypeScript or Babel major upgrade that changes metadata emission affects TSyringe more than a TSyringe version bump does.
On releases, the newest entry in the release list is v4.4.0 from 2020-11-09, while package.json carries 4.10.0. That gap is worth understanding before you pin a version: the release list and the package manifest do not line up, and the repository's last push was on 2026-09-08. Check the npm registry for the version you intend to install rather than assuming the release list is current.
What the README does not document: a migration guide for major versions, or rollback steps. If you adopt TSyringe across an existing codebase, plan the change as a refactor of construction sites, not as a reversible configuration toggle.
Editorial conclusion
TSyringe fits TypeScript codebases that already compile with experimentalDecorators and emitDecoratorMetadata, where constructor injection and token-based registration remove manual wiring. Skip it if you cannot use decorators, if you need a container that resolves without reflect-metadata, or if you want a library with frequent releases: the newest entry in the release list is v4.4.0 from 2020-11-09. Verify first that your tsconfig.json has both compiler flags, that exactly one Reflect polyfill is imported before any container call, and that your bundler keeps the metadata Babel or tsc emits.
Frequently asked questions
How do I use TSyringe?
Install the package, set experimentalDecorators and emitDecoratorMetadata in tsconfig.json, and import a Reflect polyfill once before DI is used. Then decorate a class with @injectable() and call container.resolve() on it. Interfaces need @inject("Token") because they carry no runtime type information.
Is TSyringe dead?
The repository is not archived, and its last push was on 2026-09-08. The newest entry in the release list is v4.4.0 from 2020-11-09, while package.json declares version 4.10.0, so the release list and the manifest do not match. Judge by the npm version you actually install.
What is the difference between @injectable and @singleton in TSyringe?
@injectable() lets a class have its dependencies injected at runtime, and each resolve produces an instance. @singleton() additionally registers the class as a singleton within the global container, so resolutions share one instance.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/microsoft-tsyringe)