Master Dependency Injection in TypeScript: Build Maintainable Code with IoC

This tutorial explains what dependencies are, why Dependency Injection (DI) and Inversion of Control (IoC) improve code maintainability, shows how to set up TypeScript with reflect‑metadata, and provides a full DI container implementation with Service and Inject decorators, complete usage examples and best‑practice notes.

Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Master Dependency Injection in TypeScript: Build Maintainable Code with IoC

In code, a dependency is a relationship between two modules; using Dependency Injection (DI) reduces coupling and makes code cleaner.

What is Dependency

When element A changes and causes element B to change, B depends on A. In classes this can appear as method calls, member references, or constructor parameters.

Why Dependency Injection (DI)

Consider four classes: Car, Body, Chassis, Tire. Changing the Tire size requires modifying constructors of all higher‑level classes, which is unmaintainable. By applying Inversion of Control (IoC) and DI, lower‑level classes are passed as parameters, improving maintainability and enabling easier testing.

Environment Setup

Install TypeScript and the reflect-metadata polyfill, then enable decorators in tsconfig.json:

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

Import reflect-metadata in the entry file.

Prerequisite Knowledge

Reflect

ES6 provides Reflect and Proxy APIs for object operations. The reflect-metadata library extends Reflect with metadata support, enabling metaprogramming.

Metadata Types

design:type

design:paramtypes

design:returntype

Architecture Design

Use a Map to store registered dependencies with unique keys.

Register dependencies without requiring users to define keys, using Symbol or unique strings.

Support registering classes, functions, strings, or singletons, even when decorators are unavailable.

Container

The Container class holds a map and provides set, get, and has methods.

class Container {
  private ContainerMap = new Map<string|symbol, any>();
  public set = (id: string|symbol, value: any): void => {
    this.ContainerMap.set(id, value);
  };
  public get = <T>(id: string|symbol): T => {
    return this.ContainerMap.get(id) as T;
  };
  public has = (id: string|symbol): Boolean => {
    return this.ContainerMap.has(id);
  };
}
const ContainerInstance = new Container();
export default ContainerInstance;

Service Decorator

The Service decorator registers a class as a service, optionally with a custom identifier and a singleton flag, storing metadata and an instance in the container.

export function Service(idOrSingleton?: string|boolean, singleton?: boolean): Function {
  return (target: ConstructableFunction) => {
    let _id;
    let _singleton;
    let _singleInstance;
    if (typeof idOrSingleton === 'boolean') {
      _singleton = true;
      _id = Symbol(target.name);
    } else {
      if (idOrSingleton && Container.has(idOrSingleton)) {
        throw new Error(`Service: identifier (${idOrSingleton}) already registered.`);
      }
      _id = idOrSingleton || Symbol(target.name);
      _singleton = singleton;
    }
    Reflect.defineMetadata('cus:id', _id, target);
    if (_singleton) {
      _singleInstance = new target();
    }
    Container.set(_id, _singleInstance || target);
  };
}

Inject Decorator

The Inject decorator injects a dependency into a property using stored metadata.

export function Inject(id?: string): PropertyDecorator {
  return (target: Object, propertyKey: string|symbol) => {
    const Dependency = Reflect.getMetadata('design:type', target, propertyKey);
    const _id = id || Reflect.getMetadata('cus:id', Dependency);
    const _dependency = Container.get(_id);
    Reflect.defineProperty(target, propertyKey, { value: _dependency });
  };
}

Usage

Define a logging service and mark it as a singleton:

@Service(true)
class LogService {
  public debug(...args: any[]): void { console.debug('[DEB]', new Date(), ...args); }
  public info(...args: any[]): void { console.info('[INF]', new Date(), ...args); }
  public error(...args: any[]): void { console.error('[ERR]', new Date(), ...args); }
}

Register a token in a configuration module:

export const token = '29993b9f-de22-44b5-87c3-e209f4174e39';
export default () => {
  Container.set('token', token);
};

Consume services with @Inject and Container.get:

class CustomerController {
  @Inject()
  private log!: LogService;
  private token = Container.get('token');
  public main(): void {
    this.log.info(this.token);
  }
}

Running the program prints an info log containing the token.

Notes

Decorators may execute before Container.set registrations, so use Container.get as a fallback. Parameter injection in constructors is also possible with @Inject.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Design PatternsTypeScriptIoCdependency-injectionDI Container
Tencent IMWeb Frontend Team
Written by

Tencent IMWeb Frontend Team

IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.