Detail page available
Contract & Reliability Observatory

TypeScript Contract and Reliability Observatory

We model domains and API contracts in TypeScript to catch invalid states early—while placing runtime validation at trust boundaries because types erase at runtime and do not replace tests.

Untrusted input to trusted domain pipeline

Compile-time models and runtime validation meet at explicit boundaries. Readable without JavaScript.

  • Runtime
  • Compile-time
  • Both
  • Reject path
  1. Runtime

    External input

    Network payloads, form posts, webhooks, or third-party SDK results arrive as unknown shapes.

  2. Both

    Unknown boundary

    Treat the value as unknown until a validation boundary accepts or rejects it.

  3. Runtime

    Runtime validation

    Parse with schema checks, guards, or parsers before domain code consumes the value.

  4. Reject path

    Reject path

    Invalid input becomes typed errors, retries, or user-facing recovery—not silent casts.

  5. Compile-time

    Narrowed typed value

    Successful validation produces a narrowed type the compiler can trust inside the boundary.

  6. Compile-time

    Domain model

    Map transport DTOs into domain types that encode allowed states and invariants.

  7. Compile-time

    Exhaustive handling

    Discriminated unions and never-checks force every state to be considered.

  8. Both

    Safe output / integration

    Emit responses, events, or side effects with explicit contracts and monitoring hooks.

FocusContracts & boundaries
Compile-timeStrict modeling
RuntimeValidation at edges
Proof modelTypes + tests

What we build with TypeScript

Illustrative contract systems—not a guaranteed delivery catalog.

  • Shared API and SDK contracts

    Versioned DTO and client packages with compile-time autocomplete and explicit adapters.

  • Domain state machines

    Discriminated unions for orders, permissions, and workflows with exhaustive handlers.

  • Edge validation layers

    Parsers that turn unknown network/form input into narrowed domain values before use.

TypeScript Type Boundary Lab

Select product situations to inspect untrusted input, unsafe assumptions, validation placement, and exhaustive handling. Educational models—not defect-rate guarantees.

Select product situations to inspect untrusted input, unsafe assumptions, validation placement, and exhaustive handling. Educational models—not defect-rate guarantees.

Showing type boundary model for API response.

Situation

API response

A JSON response from an internal or partner API enters the client or server handler.

Untrusted / loose input
JSON body typed as any or a hopeful interface cast after fetch.
Unsafe assumption
Assuming the remote shape always matches yesterday’s OpenAPI document.
Recommended boundary
Parse at the edge with a schema; map into a domain DTO only after success.
Runtime validation
Schema parse (for example Zod) with explicit error mapping for 4xx/5xx and malformed bodies.
Narrowed / domain type
Readonly domain object with branded IDs and literal status unions.
Exhaustive handling
Switch on status discriminants; use never for unhandled variants.

Likely failure mode

Casting response.json() to an interface hides field renames until production.

Testing consideration

Contract tests with fixture payloads for success, missing fields, and unexpected enums.

Type modeling for production decisions

Patterns chosen for invariants and refactor safety—not a syntax encyclopedia.

Primitives and branded domain values

Brand IDs and money amounts so accidental string mixing fails at compile time.

Interfaces vs type aliases

Prefer type aliases for unions and mapped shapes; interfaces when declaration merging is intentional.

Literal and discriminated unions

Encode finite business states as literal unions with a discriminant field.

Readonly transport and domain data

Mark inbound DTOs readonly to discourage accidental mutation before mapping.

Constrained generics

Constrain generics to real domain needs; avoid T extends any-shaped bags.

unknown versus any

Prefer unknown at boundaries; any disables checking and hides defects.

Optional, absent, and nullable

Separate missing keys from null when the wire format and UI mean different things.

Exhaustive switch handling

Use never checks so new union members break the build until handled.

Avoid over-generalized abstractions

A precise OrderState beats a GenericEntity<TStatus> that erases meaning.

Unknown input to narrowed domain value

Illustrative pattern—validate first, then work with a narrowed type. Not a full production stack.

type PaymentStatus = "pending" | "paid" | "failed";

type Payment = {
  readonly id: string;
  readonly status: PaymentStatus;
  readonly amountCents: number;
};

function parsePayment(input: unknown): Payment {
  if (!input || typeof input !== "object") {
    throw new Error("Invalid payment payload");
  }
  const record: Record<string, unknown> = { ...input };
  const status = record.status;
  if (status !== "pending" && status !== "paid" && status !== "failed") {
    throw new Error("Invalid payment status");
  }
  if (typeof record.id !== "string" || typeof record.amountCents !== "number") {
    throw new Error("Invalid payment fields");
  }
  return {
    id: record.id,
    status,
    amountCents: record.amountCents,
  };
}

Exhaustive handling of a discriminated union

Illustrative pattern—never forces new variants to be handled during refactors.

type OrderEvent =
  | { type: "created"; orderId: string }
  | { type: "paid"; orderId: string; receiptId: string }
  | { type: "cancelled"; orderId: string; reason: string };

function describe(event: OrderEvent): string {
  switch (event.type) {
    case "created":
      return `Order ${event.orderId} created`;
    case "paid":
      return `Order ${event.orderId} paid (${event.receiptId})`;
    case "cancelled":
      return `Order ${event.orderId} cancelled: ${event.reason}`;
    default: {
      const _exhaustive: never = event;
      return _exhaustive;
    }
  }
}

Compile-time versus runtime responsibilities

TypeScript does not validate untrusted runtime input by itself.

Compile-time

  • Shape compatibility

    Catch incompatible props, return types, and assignment mismatches before merge.

  • Narrowing assistance

    Control-flow analysis shrinks unions after guards and discriminant checks.

  • Invalid-state prevention where modeled

    Well-designed unions make illegal combinations unrepresentable in source.

  • Refactor assistance

    Rename and signature changes surface call-site breakage across the graph.

  • API authoring contracts

    Shared types document intended payloads between services and UI modules.

  • Exhaustive handling

    never checks force updates when domain states expand.

Runtime

  • Network and form validation

    Untrusted bytes and strings must be parsed before domain logic runs.

  • Authentication and authorization

    Identity and permission checks happen with real credentials and policies.

  • Business invariants

    Stock, balances, and workflow rules need executable enforcement.

  • Database constraints

    Unique keys, foreign keys, and check constraints remain source of truth.

  • Retries, timeouts, monitoring

    Operational resilience is outside the typechecker’s scope.

  • Error recovery

    User messaging and compensating actions require runtime branches.

A green typecheck increases confidence in modeled contracts. It is not proof that production input, auth, or infrastructure behaved correctly.

tsconfig decision surface

Contextual flags—not a mandate to enable every option on every repository.

strict

Intent · Enable the core strictness family for safer defaults.

Trade-off · Legacy JS interop may need staged adoption and declaration cleanup.

noUncheckedIndexedAccess

Intent · Treat index access as potentially undefined.

Trade-off · Adds narrowing noise; valuable for map/array-heavy domain code.

exactOptionalPropertyTypes

Intent · Distinguish missing properties from explicit undefined.

Trade-off · Can surprise teams used to optional === undefined assignment habits.

noImplicitOverride

Intent · Require explicit override on subclass methods.

Trade-off · Mostly relevant for class-heavy designs; low cost elsewhere.

useUnknownInCatchVariables

Intent · Force catch clauses to treat errors as unknown.

Trade-off · Requires intentional narrowing instead of assuming Error.

module / moduleResolution alignment

Intent · Keep bundler and Node resolution strategies coherent.

Trade-off · Misalignment causes path and ESM/CJS friction in monorepos.

target / lib alignment

Intent · Match emitted JS and available APIs to deployment runtimes.

Trade-off · Over-modern targets break older runtimes; over-old targets hide APIs.

isolatedModules considerations

Intent · Ensure each file can transpile independently for bundlers.

Trade-off · Forbids some cross-file-only patterns; usually correct for Next.js.

TypeScript delivery workflow

Boundary-first engineering—not generic project-management filler.

  1. 1

    Domain and boundary discovery

    Inventory external inputs, actors, and illegal states before writing interfaces.

  2. 2

    Type model and invalid-state analysis

    Design unions and brands so high-cost invalid combinations cannot be constructed.

  3. 3

    Runtime validation placement

    Put parsers at network/form edges; keep domain functions free of raw unknown.

  4. 4

    Application and integration contracts

    Version shared DTOs; isolate vendor SDK types behind adapters.

  5. 5

    Compiler-policy selection

    Choose strictness flags for the team’s codebase maturity—not every flag for every repo.

  6. 6

    Test and refactor strategy

    Pair type models with contract fixtures and regression tests for transition rules.

  7. 7

    Build, observability and maintenance

    Run tsc in CI, monitor parse failures in production, and revisit unions when domains change.

TypeScript failure modes

Constructive risks to design against during reviews and refactors.

  • Replacing any with unsafe casts

    as TrustedType silences the compiler without proving the value is trusted.

  • Trusting API responses without validation

    Interface assertions on fetch JSON hide drift until a field disappears at runtime.

  • Giant optional-property interfaces

    Everything?: optional models compile while encoding almost no invariants.

  • Generics that hide intent

    Over-abstract helpers force callers to re-learn domain rules the types erased.

  • Duplicated domain types

    Parallel User shapes across packages diverge silently during refactors.

  • Assertions masking invalid states

    Non-null assertions and double casts paper over incomplete narrowing.

  • Inconsistent nullability

    null in APIs and undefined in UI without mapping rules create intermittent defects.

  • Non-exhaustive state handling

    Default fallthrough lets new statuses ship without UI or workflow updates.

  • Mixing transport and domain models

    Vendor field names leaking into core logic couples products to wire formats.

  • Treating typecheck as production proof

    CI green means modeled contracts compiled—not that runtime paths were exercised.

Choose or reconsider TypeScript

TypeScript increases compile-time confidence. It does not provide runtime validation by itself and does not guarantee zero defects.

Choose when

  • Multiple developers share evolving API and domain contracts.
  • Illegal states are costly and should be hard to represent in source.
  • Refactoring speed and compile-time feedback matter to delivery.

Reconsider when

  • Throwaway scripts where tooling overhead outweighs collaboration benefits.
  • Teams cannot staff validation, CI typechecks, or declaration maintenance.

Prefer strict mode, ban casual any, validate at edges, keep domain unions exhaustive, and treat typecheck as necessary—not sufficient—proof.

Common TypeScript questions

Does TypeScript validate API responses automatically?

No. Types erase at runtime. Network and form input should be parsed at boundaries before domain logic runs.

Is a passing typecheck proof the product is correct?

No. Typecheck verifies modeled contracts compiled. Runtime auth, invariants, and infrastructure still need tests and operational checks.

Should every project enable every strict flag?

No. Start from strict defaults and adopt additional flags based on codebase shape and team capacity.

Implementation insight

Digital Elliptical designs TypeScript systems as contract observatories: compile-time models for valid states, runtime validation at trust boundaries, and tests for behavior the typechecker cannot see.

Next step

Discuss your TypeScript contracts

I would like to discuss TypeScript domain modeling, validation boundaries, and compiler policy for our product.

Begin stack consultation