Primitives and branded domain values
Brand IDs and money amounts so accidental string mixing fails at compile time.
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
External input
Network payloads, form posts, webhooks, or third-party SDK results arrive as unknown shapes.
Both
Unknown boundary
Treat the value as unknown until a validation boundary accepts or rejects it.
Runtime
Runtime validation
Parse with schema checks, guards, or parsers before domain code consumes the value.
Reject path
Reject path
Invalid input becomes typed errors, retries, or user-facing recovery—not silent casts.
Compile-time
Narrowed typed value
Successful validation produces a narrowed type the compiler can trust inside the boundary.
Compile-time
Domain model
Map transport DTOs into domain types that encode allowed states and invariants.
Compile-time
Exhaustive handling
Discriminated unions and never-checks force every state to be considered.
Both
Safe output / integration
Emit responses, events, or side effects with explicit contracts and monitoring hooks.
Illustrative contract systems—not a guaranteed delivery catalog.
Versioned DTO and client packages with compile-time autocomplete and explicit adapters.
Discriminated unions for orders, permissions, and workflows with exhaustive handlers.
Parsers that turn unknown network/form input into narrowed domain values before use.
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.
Situation
A JSON response from an internal or partner API enters the client or server handler.
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.
Patterns chosen for invariants and refactor safety—not a syntax encyclopedia.
Brand IDs and money amounts so accidental string mixing fails at compile time.
Prefer type aliases for unions and mapped shapes; interfaces when declaration merging is intentional.
Encode finite business states as literal unions with a discriminant field.
Mark inbound DTOs readonly to discourage accidental mutation before mapping.
Constrain generics to real domain needs; avoid T extends any-shaped bags.
Prefer unknown at boundaries; any disables checking and hides defects.
Separate missing keys from null when the wire format and UI mean different things.
Use never checks so new union members break the build until handled.
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;
}
}
}TypeScript does not validate untrusted runtime input by itself.
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.
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.
Contextual flags—not a mandate to enable every option on every repository.
Intent · Enable the core strictness family for safer defaults.
Trade-off · Legacy JS interop may need staged adoption and declaration cleanup.
Intent · Treat index access as potentially undefined.
Trade-off · Adds narrowing noise; valuable for map/array-heavy domain code.
Intent · Distinguish missing properties from explicit undefined.
Trade-off · Can surprise teams used to optional === undefined assignment habits.
Intent · Require explicit override on subclass methods.
Trade-off · Mostly relevant for class-heavy designs; low cost elsewhere.
Intent · Force catch clauses to treat errors as unknown.
Trade-off · Requires intentional narrowing instead of assuming Error.
Intent · Keep bundler and Node resolution strategies coherent.
Trade-off · Misalignment causes path and ESM/CJS friction in monorepos.
Intent · Match emitted JS and available APIs to deployment runtimes.
Trade-off · Over-modern targets break older runtimes; over-old targets hide APIs.
Intent · Ensure each file can transpile independently for bundlers.
Trade-off · Forbids some cross-file-only patterns; usually correct for Next.js.
Boundary-first engineering—not generic project-management filler.
Inventory external inputs, actors, and illegal states before writing interfaces.
Design unions and brands so high-cost invalid combinations cannot be constructed.
Put parsers at network/form edges; keep domain functions free of raw unknown.
Version shared DTOs; isolate vendor SDK types behind adapters.
Choose strictness flags for the team’s codebase maturity—not every flag for every repo.
Pair type models with contract fixtures and regression tests for transition rules.
Run tsc in CI, monitor parse failures in production, and revisit unions when domains change.
Constructive risks to design against during reviews and refactors.
as TrustedType silences the compiler without proving the value is trusted.
Interface assertions on fetch JSON hide drift until a field disappears at runtime.
Everything?: optional models compile while encoding almost no invariants.
Over-abstract helpers force callers to re-learn domain rules the types erased.
Parallel User shapes across packages diverge silently during refactors.
Non-null assertions and double casts paper over incomplete narrowing.
null in APIs and undefined in UI without mapping rules create intermittent defects.
Default fallthrough lets new statuses ship without UI or workflow updates.
Vendor field names leaking into core logic couples products to wire formats.
CI green means modeled contracts compiled—not that runtime paths were exercised.
TypeScript increases compile-time confidence. It does not provide runtime validation by itself and does not guarantee zero defects.
Prefer strict mode, ban casual any, validate at edges, keep domain unions exhaustive, and treat typecheck as necessary—not sufficient—proof.
No. Types erase at runtime. Network and form input should be parsed at boundaries before domain logic runs.
No. Typecheck verifies modeled contracts compiled. Runtime auth, invariants, and infrastructure still need tests and operational checks.
No. Start from strict defaults and adopt additional flags based on codebase shape and team capacity.
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.
I would like to discuss TypeScript domain modeling, validation boundaries, and compiler policy for our product.
Begin stack consultation