article / published note
Type Narrowing in TypeScript
Created 2026-08-31 · Updated 2026-08-31
Local review is browser-only; canonical flags are display-only.
Summary
Type narrowing gives TypeScript enough evidence to treat a broad value as a more precise type in a particular part of a program. Lauren Tan demonstrates the idea with a lexer and parser: a general Token must be narrowed before it can be passed to an AST node that accepts only boolean tokens. The article focuses on assertion functions and type guards, while also noting in, typeof, instanceof, and ordinary if statements as narrowing tools.
Why it matters
Precise types let the compiler prevent invalid states at the boundary where code needs stronger guarantees. Narrowing can make parsers and other stateful code easier to understand, but over-specifying every type can also add complexity. The useful skill is knowing where a constraint clarifies the program and where it merely makes the type system harder to work with.
Key ideas
- A union or broad object type often contains more possibilities than a function is prepared to handle.
- An assertion function can check an invariant at runtime and use an
assertsreturn signature to narrow the value for the rest of the control-flow path. - A type guard is a predicate whose return type communicates a narrowing relationship to TypeScript.
as constpreserves literal keys and values, allowing a property-checking guard to narrow a string to the keys of a keyword map.- Narrowing improves compiler feedback by connecting runtime evidence with the static type model.
- Narrowing is not a reason to make every type maximally precise; constraints should earn their complexity by making invalid states or reasoning paths clearer.
Practical applications
- Define discriminated unions for values that have a stable field identifying their variant, then narrow on that discriminator before constructing variant-specific objects.
- Use assertion functions at parser or validation boundaries when invalid input should fail immediately with a useful error.
- Use reusable type guards for property-existence checks instead of scattering unsafe casts through lookup code.
- Prefer narrowing based on runtime evidence and control flow; reserve type assertions for cases where the invariant is established elsewhere and cannot be expressed directly.
- Review highly elaborate types for whether they clarify a real invariant or only encode a theoretical possibility.
Open questions
- When does a custom assertion function provide enough value to justify maintaining its runtime error behavior?
- How should teams balance precise type-level invariants with readable APIs as a TypeScript codebase grows?
- Which parser and validation boundaries benefit most from discriminated unions instead of class-based representations?