Contracts, Interfaces & Abstraction Hierarchies
Choosing between interfaces, type aliases, and abstract classes is not stylistic — it determines bundle footprint, dynamic dispatch overhead, and architectural flexibility at public system boundaries. This article establishes mechanical decision heuristics, covers TypeScript 5+ mixins, and shows how `noImplicitOverride` prevents silent contract breakage.
TypeScript Low-Level Design & Object-Oriented Architecture
Contracts, Interfaces & Abstraction Hierarchies
In TypeScript 5+, OOP is an architectural contract, not an inheritance tree: enforce domain invariants at compile time, encapsulate mutation strictly within aggregates, and invert dependencies so high-level business policy never couples to execution details. The word "contract" in that principle is precise — it refers to the interface declarations that define what a collaborator must provide, independent of how it provides it.
The choice between interface, type alias, and abstract class is frequently made by habit rather than engineering judgment. That default costs real money: choosing abstract class when an interface suffices adds JavaScript to the bundle, creates prototype chain entries in V8, and introduces single-inheritance constraints that limit architectural flexibility. This article builds mechanical decision heuristics so that choice is never accidental.
1. The Anti-Pattern Graveyard: The Diamond Inheritance Breakdown
Here is an abstraction hierarchy common in codebases that apply OOP patterns from Java directly to TypeScript without adapting to the structural type system:
The problem is not that the class hierarchy is deep — it is that each layer adds fields that a test must satisfy to instantiate the class at all. Unit testing calculateInterest should require only balance and interestMultiplier. Instead, it requires satisfying id, createdAt, updatedAt, updatedBy, accountType, and balance before the test can even begin. The inheritance hierarchy exported its construction burden to every test file.
The fix is a combination of interface composition and targeted abstract class usage: use interface for audit and identity contracts, use abstract class only for calculateInterest — the one piece that has shared implementation logic.
2. interface vs type vs abstract class: The Decision Matrix
2.1 Compile-Time Erasure vs. Runtime Emission
The most fundamental distinction: interface and type are TypeScript-only constructs that emit zero JavaScript. They exist only during type checking and are completely removed at compilation. abstract class and class are JavaScript constructs that emit executable prototype code that runs in V8.
Every abstract class you declare adds a class definition to your bundle. At scale, across hundreds of files, this is measurable.
2.2 The Three-Way Decision Heuristic
| Scenario | Use |
|---|---|
| Public API contract — describes what external code must implement | interface — erases to zero bytes, supports declaration merging |
| Union / intersection / mapped / conditional type — structural transformation | type alias — richer type algebra than interface |
| Shared implementation — multiple concrete classes share actual logic (not just a shape) | abstract class — justified because there is real code to inherit |
| Multiple inheritance of behavior | Mixin functions (const Timestampable = <T extends Constructor>(Base: T)) — abstract class cannot extend multiple bases |
2.3 Declaration Merging: The interface-Only Superpower
This is used by major libraries (Express, React types) to allow third-party packages to augment existing interfaces without modifying the original type definition. type aliases do not support merging — attempting to redeclare a type is an error.
3. Multiple Contract Implementation & Collision Safety
TypeScript allows a class to implement multiple interfaces simultaneously. When two interfaces declare methods with the same name, the class must provide a single implementation that is compatible with both signatures.
When two interfaces declare the same method name with incompatible signatures, TypeScript issues a compile-time error at the implements clause, preventing the ambiguity from reaching runtime.
4. noImplicitOverride and Safe Inheritance Contracts
TypeScript 4.3 introduced the --noImplicitOverride compiler flag, which enforces that any method in a derived class that shadows a parent method must be decorated with the override keyword. This prevents a particularly insidious class of production bug: a base class method is renamed or removed, and the derived class silently stops overriding anything without any compiler warning.
4.1 Without noImplicitOverride
4.2 With noImplicitOverride + override
Enable "noImplicitOverride": true in tsconfig.json for all new TypeScript projects. For existing codebases, migrate gradually — add override to intended overrides, catch unintentional ones.

5. Abstract Classes: Template Method Pattern
When a class hierarchy genuinely shares implementation logic, abstract class is justified. The Template Method pattern defines an algorithmic skeleton in the abstract base and delegates specific steps to concrete subclasses.
The abstract class is justified here because:
- Both
StripeProcessorandAdyenProcessormust executevalidateandauditidentically. - The
processmethod orchestration logic is shared and cannot differ across implementations. - There is genuine code inheritance — not just shape inheritance.
6. TypeScript 5+ Mixins: Composable Multi-Trait Classes
Class inheritance in TypeScript is single-chain: a class can only extend one base class. When multiple orthogonal capabilities must be composed — auditing, timestamping, soft-deletion — class mixins using higher-order factory functions provide a clean, multiple-inheritance-free solution.
6.1 Mixin Pattern Implementation
6.2 Mixin Composition Diagram
Cross-Language Rosetta — Mixins:
- Go: Achieved via struct embedding.
type Order struct { Timestampable; Auditable; ... }. Methods from embedded types are promoted without explicit delegation. - Python: Multiple inheritance with
super()MRO (Method Resolution Order).class Order(SoftDeletable, Auditable, Timestampable, BaseEntity). - TypeScript: Mixin functions — no language-level multiple inheritance, but factory function composition produces equivalent semantics without diamond problem.
7. Recursive Generic Constraints
When a base class needs to return this-typed values that preserve the derived class's type, but must also express this in a generic signature, recursive constraints are the tool:
The recursive constraint T extends Entity<T> is the TypeScript idiom for "the self-referential type that represents the most-derived class." It is the compile-time equivalent of Class<? extends Self> generic bounds found in Rust and Swift.
Summary
| Concept | Rule |
|---|---|
interface |
Zero bundle cost — use for public contracts, declaration merging, and structural shapes |
type alias |
Use for unions, intersections, mapped/conditional types, and transforms |
abstract class |
Non-zero bundle cost — justify only with shared implementation (Template Method) |
noImplicitOverride |
Enable in every project; override keyword makes parent contract dependency explicit |
| Mixins | Higher-order factory functions compose orthogonal capabilities without multiple-inheritance collisions |
| Declaration merging | interface-only — allows third-party packages to extend existing types non-invasively |
| Recursive constraints | T extends Class<T> captures the self-referential type of the most-derived class |
What's Next
In Part 4, we move from defining contracts to managing the lifecycles of the objects that implement them. Composition vs. aggregation, circular module dependencies, memory leaks from zombie event listeners, and TypeScript 5.2+
using/Symbol.disposefor deterministic resource cleanup — the mechanics that keep a production fintech service from gradually degrading under load. Part 4: Object Lifecycles, Coupling & Domain Aggregates covers the full DDD aggregate model built on top of the foundations established in Parts 1–3.
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.