D
DEV.LOG
Search Publications
Press ESC or ⌘K to exitFast Static Indexing
High-tech matrix digital code and architectural system integrity
Architecture
Aug 25, 20262 min read

The Discipline of the Type System: Clean Architecture & Domain Invariants in TypeScript

Master strict TypeScript types, discriminated unions, nominal branding, and exhaustiveness checking to eliminate runtime errors and model domain invariants cleanly.

Alex Morgan
Alex Morgan
Software Engineer & Creative Developer
CORE TECHNIQUEThe Rule of Make Impossible States Impossible

In domain-driven frontend engineering, types are not just annotations for IDE autocomplete; they are mathematical boundaries that guarantee illegal states can never exist at runtime.

The Discipline of the Type System

A well-crafted type system prevents entire classes of software defects before a single line of code is executed in production. In modern TypeScript development, our type declarations act as verifiable contracts between distributed systems, business logic layers, and UI components.

Alex Morgan
// Software Engineer & Creative Developer

“Avoid ambiguous boolean flags! Model states as explicit Discriminated Unions and leverage nominal branding so the compiler enforces total exhaustiveness across every branch.”


1. Discriminated Unions: Eliminating Ambiguous State

Instead of loose optional fields and multiple boolean flags that allow contradictory states (e.g. isLoading: true and isSuccess: true simultaneously), structure state transitions as tagged unions:

src/domain/payment.ts
// ❌ Anti-Pattern: Ambiguous and impossible states permitted
type AmbiguousPaymentState = {
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
errorMessage?: string;
transactionId?: string;
};
// ⚡ Clean Architecture: Explicit Discriminated Union
export type PaymentState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; transactionId: string; timestamp: Date }
| { status: 'failed'; error: Error; retryCount: number };
📜ARCHITECTURE NOTECompiler-Guaranteed Exhaustiveness Checking

By leveraging TypeScript’s never type, the compiler verifies that every domain state case is handled. If you add a new state in the future, the compiler will refuse to build until all handlers are updated.

src/domain/handler.ts
import type { PaymentState } from './payment';
export function handlePayment(state: PaymentState): string {
switch (state.status) {
case 'idle':
return 'Ready for customer submission';
case 'loading':
return 'Processing transaction with payment gateway...';
case 'success':
return `Transaction completed successfully: ${state.transactionId}`;
case 'failed':
return `Transaction failed: ${state.error.message} (Retry attempt ${state.retryCount})`;
default:
const _exhaustiveCheck: never = state;
return _exhaustiveCheck;
}
}

2. Branded Types for Domain Integrity

Prevent accidental transposition of primitive IDs (such as passing a UserId into a function requiring an OrderId) through nominal type branding:

Nominal Type Branding

// ARCHITECTURE PANEL
Type Branding Definition
type Brand<K, T> = K & { readonly __brand: T };

export type UserId = Brand<string, 'UserId'>;
export type OrderId = Brand<string, 'OrderId'>;
Compile-Time Safety

Attempting to assign an OrderId to a variable typed as UserId produces a strict compile error, eliminating parameter transposition bugs in critical domain pipelines.

Type Invariant Safety
Alex Morgan
Written by

Alex Morgan

Software Engineer & Creative Developer

Frontend engineer and creative developer fascinated by the craft of building blazingly fast web apps, liquid glass design systems, and resilient software architectures.