Skip to main content

TypeScript analyzer

Design Principles

Dependency-inversion and interface-segregation findings the type checker can prove: constructor parameters typed by nominal concrete classes, services constructed inside methods instead of injected, and behavioral interfaces whose consumers each use a sliver.

patterns ts-design-principles

Design Principles

Dependency-inversion and interface-segregation findings the type checker can prove: constructor parameters typed by nominal concrete classes, services constructed inside methods instead of injected, and behavioral interfaces whose consumers each use a sliver.

Syntax-only "SOLID checkers" guess class identity from names, and the guess is wrong often enough to make them unreliable — this codebase's own name-based concrete-import heuristic was measured at 60 % false on schema-heavy files. This family runs in the same tsconfig-driven, fully type-checked pass as the type-aware checks and reports only what the checker proves: a constructor parameter whose concrete class carries private or protected members is NOMINAL (TypeScript compares it by declaration identity, so no structurally-shaped test double can satisfy it); a service constructed inside an instance method is a dependency the caller cannot see or replace; a behavioral interface whose consumers each touch a small share of its methods is carrying several roles under one name. Every exemption is a measured false-positive boundary: composition roots are recognised by how many collaborators they construct (never by name), conditional construction is a factory, value objects may be constructed freely, and data-record interfaces are never judged for segregation. Findings are informational: they name the mechanism, and the principle is the explanation, not a verdict.

Severity guide

info
All three templates are informational — design seams to examine, ranked within the repository, never scored.
warning
Not emitted by this family.
critical
Not emitted by this family.

Examples

Before

class Scheduler {
  constructor(private readonly snapshots: HistoricalSnapshotService) {}
}

After

interface SnapshotSource { latest(): Promise<Snapshot | null> }
class Scheduler {
  constructor(private readonly snapshots: SnapshotSource) {}
}

`HistoricalSnapshotService` declares private members, so the parameter type is nominal: tests must construct the real service or cast through `unknown`. An interface of the members actually used is satisfiable by any structurally compatible object.

Remediation

Depend on the shape you use: inject collaborators, type them by interfaces, and split interfaces along consumer seams.

For nominal dependencies, declare an interface (or a Pick<> of the members used) and type the constructor parameter by it; the concrete class still satisfies it. For hidden collaborators, move construction to the composition root and pass the instance in — or accept a factory when the collaborator must be created per call. For fat interfaces, extract a role interface per consumer seam and let the original extend them; implementors change nothing.

Documentation