Loading...

Skip to content
MemonaIQ
ToolsLearnCompany
Sign in
ToolsLearnCompanyBrowse all tools
Knowledge Center
In this topic
Software Engineering
SOLID principles explained, with practical examplesDRY, KISS and YAGNI: clean code principles in practiceDependency injection explained, with TypeScript examplesClean architecture: the dependency rule, ports and adaptersMonolith vs microservices: trade-offs and when to splitRace conditions: causes, examples and how to prevent themMessage queues and event-driven architecture explainedThe test pyramid: unit, integration and end-to-end tests
A-Z glossary
HomeKnowledgeSoftware Engineering
Software Engineering•4 min read•intermediate

Dependency injection explained, with TypeScript examples

What dependency injection is, how constructor injection works in TypeScript, how it relates to IoC and DI containers, and when a function argument is enough.

What dependency injection means

Dependency injection (DI) means a piece of code receives the objects it needs from outside instead of creating them itself. Typical dependencies are a database client, an HTTP client or a clock. The most common form is constructor injection: a class lists its dependencies as constructor parameters, and whoever creates it passes them in.

Without it, a class that calls new PostgresClient() in its constructor is tied to that database. You can't test its logic without a running database, and switching storage means editing the class.

With constructor injection, the class asks for something that matches an interface. The choice of implementation moves to the code that assembles the application. Setter and property injection also exist, but constructor injection makes dependencies explicit and the object usable as soon as it exists.

Constructor injection in TypeScript

In the example below, ReminderService needs invoice data and the current time. It declares both as interfaces and receives them in its constructor. It doesn't know whether invoices come from PostgreSQL, an API or an in-memory array.

The one place that creates the real implementations is called the composition root. It is usually the application's entry point or a module that wires services together, and keeping construction there makes the dependency graph easy to find.

reminder-service.ts
typescript
type Invoice = { id: string; dueDate: Date; paidAt: Date | null };

interface InvoiceRepository {
  findOverdue(asOf: Date): Promise<Invoice[]>;
}

interface Clock {
  now(): Date;
}

class ReminderService {
  constructor(
    private readonly invoices: InvoiceRepository,
    private readonly clock: Clock,
  ) {}

  async overdueCount(): Promise<number> {
    const overdue = await this.invoices.findOverdue(this.clock.now());
    return overdue.length;
  }
}

// Composition root: the one place that picks real implementations.
// PostgresInvoiceRepository and pool are defined elsewhere.
const service = new ReminderService(
  new PostgresInvoiceRepository(pool),
  { now: () => new Date() },
);

// In a test: small hand-written fakes, no database.
const testService = new ReminderService(
  { findOverdue: async () => [{ id: 'inv_1', dueDate: new Date('2026-01-01'), paidAt: null }] },
  { now: () => new Date('2026-02-01T00:00:00Z') },
);

Inversion of control vs DI containers

Inversion of control (IoC) is the broader idea: your code hands control of some decision to something outside it. A framework that calls your route handler applies IoC to control flow. Dependency injection applies it to one decision, which is who creates a dependency.

A DI container is a library that automates the wiring. You register which implementation goes with each interface or token, and the container builds objects and their dependencies for you. Examples include the injectors in Angular and NestJS, Spring in Java and Microsoft.Extensions.DependencyInjection in .NET.

A container is optional. Wiring by hand in a composition root, sometimes called pure DI, is plain code the compiler checks. Containers pay off in large object graphs with scoped lifetimes, such as one database transaction per request. They also add decorators, registration rules and errors that appear only at runtime when a binding is missing.

A constructor that takes eight dependencies is a warning sign either way. The class probably has too many responsibilities, and splitting it helps more than a container would.

How dependency injection helps testing

Injected dependencies let a test replace slow or unpredictable parts with simple fakes. An in-memory repository returns fixed records and a fake clock returns a fixed date. A test for overdue invoices then gives the same result every day and runs without a database.

Prefer small hand-written fakes over mocks that assert every call. Tests that check which methods were called, in which order, break on every refactor even when the behavior is unchanged.

Fakes have a limit: they prove your logic works against the fake. You still need integration tests that run the real adapter against a real database, because that is where query errors, missing constraints and migration problems show up.

When a plain function argument is enough

You don't need classes or a container to get the benefits. A function that takes its dependencies as parameters is dependency injection in its simplest form. A default value keeps production call sites short while tests pass something else, as in the example below.

Don't inject what never needs replacing. Pure helpers, such as a date formatter, and stable standard library functions can be imported directly. Inject what touches the outside world or varies between environments: storage, network calls, time, randomness and configuration.

is-overdue.ts
typescript
type Invoice = { id: string; dueDate: Date; paidAt: Date | null };

export function isOverdue(invoice: Invoice, now: Date = new Date()): boolean {
  return invoice.paidAt === null && invoice.dueDate < now;
}

// Production: isOverdue(invoice)
// Test:       isOverdue(invoice, new Date('2026-02-01T00:00:00Z'))
Previous guide
DRY, KISS and YAGNI: clean code principles in practice
Next guide
Clean architecture: the dependency rule, ports and adapters

On this page

What dependency injection meansConstructor injection in TypeScriptInversion of control vs DI containersHow dependency injection helps testingWhen a plain function argument is enough
Updated:2026-10-25
Format:Technical Guide
MemonaIQ

Focused tools for everyday digital work.

  • 18 focused tools
  • 9 run in your browser
Browse all tools Get help

Featured tools

URL Intelligence PlatformImage CompressorInvoice StudioDNS LookupAI Token Counter

Explore

All toolsKnowledge CenterGuidesResourcesCompany

Support

HelpContactSecurity

Legal

PrivacyTermsCookies

© 2026 MemonaIQ. Made in Colombo.

Back to top

MemonaIQ