Fundamentals 23 min read

Inversion of Control and SOLID Principles in the JavaScript Ecosystem with InversifyJS and Theia

This article revisits Inversion of Control and SOLID design principles within the JavaScript ecosystem, explains their practical application using InversifyJS, demonstrates a full‑stack example in the Theia IDE framework, and discusses the benefits, challenges, and best‑practice considerations of adopting IOC in modern web projects.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Inversion of Control and SOLID Principles in the JavaScript Ecosystem with InversifyJS and Theia

Introduction

Inversion of Control (IoC) and the SOLID design principles are mature concepts that have been proven in traditional software development, and this article re‑examines them from a JavaScript perspective, using popular tooling and real‑world examples.

What is IoC?

A React Context example shows how a low‑level component (Avatar) can be decoupled from its consumers, illustrating the core idea of IoC: the lower‑level component no longer creates or knows the concrete implementation of its dependencies.

The article emphasizes that IoC is not a single technology but a methodology that addresses architecture, collaboration, and long‑term maintainability.

Modules vs. IoC

Using simple module imports can appear sufficient for small projects, but it introduces hidden coupling, concrete‑implementation dependencies, potential circular dependencies, and uncontrolled lifecycles.

InversifyJS – The Most Popular IoC Container in the JavaScript Ecosystem

InversifyJS is a lightweight (≈4 KB) IoC container for TypeScript and JavaScript that aims to help developers write SOLID‑compliant code, promote best OOP practices, keep runtime overhead low, and provide a pleasant developer experience.

Getting Started with InversifyJS

Step 1 – Declare Interfaces and Types

// file interfaces.ts
interface Warrior {
    fight(): string;
    sneak(): string;
}
interface Weapon {
    hit(): string;
}
interface ThrowableWeapon {
    throw(): string;
}

Step 2 – Define Symbols for Identifiers

// file types.ts
const TYPES = {
    Warrior: Symbol.for("Warrior"),
    Weapon: Symbol.for("Weapon"),
    ThrowableWeapon: Symbol.for("ThrowableWeapon")
};
export { TYPES };

Step 3 – Use @injectable and @inject Decorators

// file entities.ts
import { injectable, inject } from "inversify";
import "reflect-metadata";

@injectable()
class Katana implements Weapon {
    public hit() { return "cut!"; }
}

@injectable()
class Shuriken implements ThrowableWeapon {
    public throw() { return "hit!"; }
}

@injectable()
class Ninja implements Warrior {
    private _katana: Weapon;
    private _shuriken: ThrowableWeapon;
    public constructor(
        @inject(TYPES.Weapon) katana: Weapon,
        @inject(TYPES.ThrowableWeapon) shuriken: ThrowableWeapon
    ) {
        this._katana = katana;
        this._shuriken = shuriken;
    }
    public fight() { return this._katana.hit(); }
    public sneak() { return this._shuriken.throw(); }
}
export { Ninja, Katana, Shuriken };

Step 4 – Create and Configure the Container

// file inversify.config.ts
import { Container } from "inversify";
import { TYPES } from "./types";
import { Warrior, Weapon, ThrowableWeapon } from "./interfaces";
import { Ninja, Katana, Shuriken } from "./entities";

const myContainer = new Container();
myContainer.bind
(TYPES.Warrior).to(Ninja);
myContainer.bind
(TYPES.Weapon).to(Katana);
myContainer.bind
(TYPES.ThrowableWeapon).to(Shuriken);
export { myContainer };

Step 5 – Resolve Dependencies

import { myContainer } from "./inversify.config";
import { TYPES } from "./types";
import { Warrior } from "./interfaces";

const ninja = myContainer.get
(TYPES.Warrior);
expect(ninja.fight()).eql("cut!");
expect(ninja.sneak()).eql("hit!");

Advantages of InversifyJS

Decouples implementation from abstraction, enabling easy swapping and AB testing.

Centralises all bindings in a single container, simplifying maintenance.

Supports TypeScript type safety, lazy injection, multi‑injection, optional dependencies, and scoped lifecycles (Transient, Singleton, Request).

Provides middleware, interceptors, and developer tools for debugging.

Dive Into Theia – A Real‑World Example

Theia is a cloud‑ and desktop‑ready IDE framework built with modern web technologies (TypeScript, VS Code‑like architecture). It consists of a frontend and a backend communicating via JSON‑RPC, both using InversifyJS for dependency injection.

Theia Architecture

The frontend runs in the browser or Electron, loading extension‑provided DI modules and starting FrontendApplication . The backend runs on Node.js (Express) and starts BackendApplication . Both sides register services through Inversify containers.

File‑Search Extension Example

The article walks through the file‑search extension, showing the common interface definition, backend implementation, and frontend UI logic, all wired via Inversify bindings.

// backend module (node/file-search-service-impl.ts)
@injectable()
export class FileSearchServiceImpl implements FileSearchService {
    constructor(
        @inject(ILogger) protected readonly logger: ILogger,
        @inject(RawProcessFactory) protected readonly rawProcessFactory: RawProcessFactory
    ) {}
    async find(searchPattern: string, options: FileSearchService.Options, clientToken?: CancellationToken): Promise
{
        // implementation omitted for brevity
    }
    // additional private helper methods omitted
}

The corresponding frontend module creates a proxy to the backend service and registers a quick‑access command ( Ctrl+P ) to open files.

Extending Theia

Because all components are bound by identifiers (symbols) and injected, developers can replace any service (e.g., QuickFileOpenService ) by providing a new implementation and rebinding it in their own extension, without modifying the core code.

Limitations of IoC

JavaScript projects are not always OOP‑centric, so applying traditional IoC/SOLID concepts may feel unnatural.

Effective use of IoC requires a learning curve and careful design of interfaces.

Rapidly changing requirements can make upfront interface design challenging.

Conclusion

The article highlights how IoC and SOLID principles, when combined with a robust container like InversifyJS, can bring clarity, testability, and flexibility to large JavaScript codebases such as Theia, while also acknowledging the practical challenges of adopting these patterns.

software architectureTypeScriptDependency InjectionInversion of ControlInversifyJSTheia
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login 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.