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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
