Reading time: ~8 min
Why narrowing matters
Writing TypeScript that « just works » is less about fancy types and more about narrowing values at the right time. In this guide, you’ll learn practical patterns custom type guards, discriminated unions, exhaustive switch to make the compiler your teammate. We’ll start from unknown, validate real-world data, and end with the safety of never to catch impossible states before they ship. Whether you’re refactoring a legacy codebase or hardening a new API layer, these patterns boost IntelliSense, reduce runtime bugs, and make code reviews faster. Let’s turn vague shapes into predictable types step by step.
Start safe with unknown
function readValue(input: unknown) {
if (typeof input === 'string') {
return input.trim();
}
if (typeof input === 'number') {
return input.toFixed(2);
}
return null;
}
Use unknown for untrusted data, then narrow.
Custom type guards (returning arg is Type)
type User = { id: string; email: string };
function isUser(x: unknown): x is User {
return !!x && typeof x === 'object'
&& 'id' in (x as any)
&& 'email' in (x as any);
}
function greet(x: unknown) {
if (isUser(x)) {
return `Hi ${x.email}`;
}
return 'Hi there';
}Discriminated unions for shape-safe branching
type LoadState =
| { kind: 'idle' }
| { kind: 'loading' }
| { kind: 'success'; data: string[] }
| { kind: 'error'; error: string };
function render(state: LoadState) {
switch (state.kind) {
case 'idle': return 'Click Load';
case 'loading': return 'Loading…';
case 'success': return state.data.join(', ');
case 'error': return `Oops: ${state.error}`;
default: {
const _exhaustive: never = state; // forces exhaustive checks
return _exhaustive;
}
}
}The never trick for exhaustiveness
Adding a new variant (e.g., { kind: ’empty’ }) now breaks the switch—exactly what we want.
As const, satisfies, and readonly
const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = typeof ROLES[number]; // 'admin' | 'editor' | 'viewer'
const config = {
env: 'prod',
retry: 3,
} as const satisfies { env: 'prod' | 'dev'; retry: number };Safer indexing: noUncheckedIndexedAccess
Enable in tsconfig.json to get string | undefined when indexing arrays/records.
Pattern library—copy/paste
- Truthy guard:
if (value) { /* narrowed */ } - In-operator guard:
if ('id' in obj) { … } - Array guard: `
Array.isArray(v) - Nullish guard: if (v != null) { … }
FAQ
unknown—it forces you to validate and narrow.