Type Narrowing
MedType narrowing is the process by which TypeScript refines a broad type to a more specific one within a conditional branch. When you use `typeof`, `instanceof`, `in`, or custom type guards, TypeScript tracks the control flow and narrows the type so you can safely access properties or methods that only exist on the narrowed type.
Interactive Visualization
Key Points
- `typeof` narrows primitive types: string, number, boolean, symbol, bigint, function, object, undefined
- `instanceof` narrows class instances by checking the prototype chain
- The `in` operator narrows by checking if a property exists on the object
- Discriminated unions use a common literal property to narrow between variants
- Custom type guards (functions returning `x is Type`) enable reusable narrowing logic
- Truthiness checks narrow out `null`, `undefined`, `0`, `""`, and `false`
- The `never` type appears when all possibilities are exhausted — useful for exhaustive checks
Code Examples
typeof & instanceof Guards
function format(value: string | number | Date): string { if (typeof value === 'string') { return value.trim() // narrowed to string } if (typeof value === 'number') { return value.toFixed(2) // narrowed to number } // TypeScript knows value is Date here return value.toISOString() } class ApiError extends Error { statusCode: number constructor(message: string, statusCode: number) { super(message) this.statusCode = statusCode } } function handleError(err: Error) { if (err instanceof ApiError) { console.log(err.statusCode) // narrowed to ApiError } }
typeof works for primitives, instanceof for class instances. TypeScript follows your control flow and narrows the type in each branch automatically.
Discriminated Unions
type Shape = | { kind: 'circle'; radius: number } | { kind: 'square'; side: number } | { kind: 'rectangle'; width: number; height: number } function area(shape: Shape): number { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2 case 'square': return shape.side ** 2 case 'rectangle': return shape.width * shape.height } } // Exhaustive check helper function assertNever(x: never): never { throw new Error(`Unexpected value: ${x}`) }
Discriminated unions use a shared literal property (like `kind`) as a discriminant. TypeScript narrows the union in each case branch, giving you access to variant-specific properties.
Custom Type Guards
interface Fish { swim: () => void } interface Bird { fly: () => void } // Custom type guard — returns a type predicate function isFish(pet: Fish | Bird): pet is Fish { return 'swim' in pet } function move(pet: Fish | Bird): void { if (isFish(pet)) { pet.swim() // narrowed to Fish } else { pet.fly() // narrowed to Bird } } // Assertion function — throws if assertion fails function assertString(val: unknown): asserts val is string { if (typeof val !== 'string') { throw new TypeError('Expected a string') } }
Custom type guards let you encapsulate narrowing logic in reusable functions. The `pet is Fish` return type tells TypeScript to narrow the parameter in the truthy branch.
Common Mistakes
- Forgetting that `typeof null` returns "object" — null must be checked separately
- Using `==` instead of `===` which can coerce types and confuse narrowing
- Not handling all cases in a discriminated union (missing exhaustive check)
- Writing type guards that do not actually narrow correctly at runtime
- Relying on truthiness checks when `0` or `""` are valid values
Interview Tips
- Discriminated unions are a favorite interview topic — know the pattern cold
- Explain the `never` type and exhaustive checking with `assertNever`
- Custom type guards show advanced TypeScript knowledge — mention `is` predicates
- Know the difference between type assertions (`as`) and type guards (runtime checks)