Type Narrowing
How TypeScript refines types through control flow — predicates, discriminants, and patterns that keep strict mode honest.
intermediate2 min read
- typescript
- narrowing
- types
Strict TypeScript is only useful if you narrow unions instead of asserting them away. Narrowing is how control flow becomes type safety.
Built-in guards
function len(x: string | string[]) {
if (typeof x === 'string') return x.length;
return x.length; // string[]
}
function process(x: string | null) {
if (x == null) return;
// x is string
console.log(x.toUpperCase());
}
typeof, instanceof, equality checks, and in all narrow.
Discriminated unions
Model state machines explicitly:
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function view(state: RequestState<User>) {
switch (state.status) {
case 'idle':
return 'Start';
case 'loading':
return '…';
case 'success':
return state.data.name;
case 'error':
return state.error.message;
}
}
The discriminant (status) is the key. Avoid optional fields like data?: T; error?: Error for mutually exclusive states.
User-defined type predicates
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
const pets: (Fish | Bird)[] = [];
pets.filter(isFish).forEach((f) => f.swim());
Prefer genuine runtime checks over as casts.
Assertion functions
function assertDefined<T>(x: T | null | undefined, msg: string): asserts x is T {
if (x == null) throw new Error(msg);
}
After assertDefined(user), TypeScript treats user as defined.
What not to do
// Escaping the type system — last resort only
const el = document.querySelector('.app') as HTMLDivElement;
Prefer:
const el = document.querySelector('.app');
if (!(el instanceof HTMLDivElement)) throw new Error('missing .app');
Interview takeaway
Explain narrowing as control-flow analysis. Show a discriminated union for async UI state. Mention predicates when filters must preserve types.
Related guides
- Generics BasicsType parameters for functions, components, and data structures — constraints, inference, and the mistakes that produce any-shaped APIs.
- Basic Types and AnnotationsPrimitives, arrays, objects, and function annotations TypeScript actually checks — plus where inference is enough and where it is not.
- Branded Types for IDsNominal-style UserId vs OrderId in TypeScript — prevent ID mixups at compile time with brands, parsers, and form boundaries.
- Conditional Types IntroT extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.
- Declaration Files and DefinitelyTypedHow .d.ts files describe JS to TypeScript, when to use @types packages, module augmentation, and writing minimal ambient types for untyped libs.