TypeScript Practices That Keep Boundaries Honest
Strict types, validated inputs, and small domain models that make invalid states harder to represent.
Treat external data as untrusted
TypeScript checks code during development; it does not validate JSON returned by an API or values read from storage. Keep the boundary explicit: receive external values as unknown, validate their shape, then pass a trusted domain type into the application.
interface User {
id: number;
name: string;
role: 'admin' | 'member';
}
function isUser(value: unknown): value is User {
if (typeof value !== 'object' || value === null) return false;
const candidate = value as Record<string, unknown>;
return typeof candidate.id === 'number'
&& typeof candidate.name === 'string'
&& (candidate.role === 'admin' || candidate.role === 'member');
}
The cast is contained inside the validator. Code after that boundary can use User without spreading assertions across the codebase.
Model states instead of combining booleans
Several independent flags can describe impossible combinations such as loading and success at the same time. A discriminated union makes the valid states visible:
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; message: string };
Rendering code can now switch on one property, and exhaustive checks reveal missing cases when the model changes.
Keep inference, preserve constraints
Use inference for local implementation details and explicit types at public boundaries. The satisfies operator is useful for configuration because it checks the contract without widening every literal value.
Strict TypeScript is most valuable when it clarifies ownership: validate at system edges, model the domain directly, and avoid assertions that merely silence uncertainty.
Further reading: TypeScript narrowing and satisfies.