0
0
Typescriptprogramming~15 mins

Exhaustive pattern matching in Typescript - Deep Dive

Choose your learning style9 modes available
Overview - Exhaustive pattern matching
What is it?
Exhaustive pattern matching is a way to check all possible cases of a value, making sure none are missed. It helps you write code that handles every option explicitly, so your program doesn't behave unexpectedly. In TypeScript, this often involves using types and switch statements or conditional checks to cover all variants of a type. This makes your code safer and easier to understand.
Why it matters
Without exhaustive pattern matching, your program might ignore some cases, leading to bugs or crashes that are hard to find. It ensures your code handles every possible input, which is especially important in complex systems or when working with different data shapes. This reduces errors and makes your software more reliable and maintainable.
Where it fits
Before learning exhaustive pattern matching, you should understand TypeScript's type system, union types, and basic control flow like if statements and switch cases. After mastering it, you can explore advanced type narrowing, discriminated unions, and functional programming patterns that rely on safe and complete case handling.
Mental Model
Core Idea
Exhaustive pattern matching means checking every possible case of a value so nothing is left unhandled.
Think of it like...
It's like checking every room in a house to make sure no one is left inside before locking the door.
Value to check
  │
  ├─ Case A ── handled
  ├─ Case B ── handled
  ├─ Case C ── handled
  └─ No case left unhandled (exhaustive)
Build-Up - 7 Steps
1
FoundationUnderstanding union types basics
🤔
Concept: Union types let a value be one of several types, which is the foundation for pattern matching.
In TypeScript, you can define a variable that can hold different types using union types. For example: const value: 'red' | 'green' | 'blue' = 'red'; This means value can be 'red', 'green', or 'blue'.
Result
You can assign any of the specified values to the variable, but nothing else.
Knowing union types is essential because pattern matching works by checking which type or value from the union you have.
2
FoundationBasic switch statement usage
🤔
Concept: Switch statements let you run different code depending on the value, a simple form of pattern matching.
You can use a switch statement to check a variable's value and run code for each case: switch(value) { case 'red': console.log('Stop'); break; case 'green': console.log('Go'); break; case 'blue': console.log('Calm'); break; }
Result
The program prints a message depending on the value of 'value'.
Switch statements are the basic tool for pattern matching, but they don't guarantee you handle all cases.
3
IntermediateRecognizing non-exhaustive matches
🤔Before reading on: do you think a switch statement always forces you to handle every case in a union type? Commit to yes or no.
Concept: Switch statements do not force you to cover all union cases, which can lead to missed cases.
If you forget a case in a switch, TypeScript won't always warn you. For example: const value: 'red' | 'green' | 'blue' = 'red'; switch(value) { case 'red': console.log('Stop'); break; case 'green': console.log('Go'); break; // 'blue' case missing } This code ignores 'blue' without error.
Result
The program runs but silently ignores some cases, risking bugs.
Understanding that switch alone doesn't guarantee exhaustiveness helps you look for better ways to ensure all cases are handled.
4
IntermediateUsing never type for exhaustiveness
🤔Before reading on: do you think TypeScript's 'never' type can help catch missing cases in pattern matching? Commit to yes or no.
Concept: The 'never' type represents impossible values and can be used to detect unhandled cases.
You can add a default case that assigns the value to a variable of type 'never'. If any case is missing, TypeScript will show an error: function assertNever(x: never): never { throw new Error('Unexpected value: ' + x); } switch(value) { case 'red': // handle break; case 'green': // handle break; case 'blue': // handle break; default: assertNever(value); // Error if any case missing }
Result
TypeScript forces you to handle all cases or get a compile error.
Using 'never' in this way turns pattern matching into a safe, exhaustive check.
5
IntermediateDiscriminated unions for complex types
🤔
Concept: Discriminated unions use a common property to distinguish types, enabling exhaustive pattern matching on objects.
Instead of simple strings, you can have objects with a 'type' field: type Shape = | { type: 'circle'; radius: number } | { type: 'square'; size: number }; function area(shape: Shape) { switch(shape.type) { case 'circle': return Math.PI * shape.radius ** 2; case 'square': return shape.size ** 2; default: assertNever(shape); // Ensures all shapes handled } }
Result
You can safely handle all shape types with clear code.
Discriminated unions let you pattern match on complex data safely and clearly.
6
AdvancedExhaustiveness with type narrowing
🤔Before reading on: do you think TypeScript narrows types automatically in all if-else chains? Commit to yes or no.
Concept: TypeScript narrows types based on conditions, but you must structure code carefully to keep exhaustiveness checks effective.
Using if-else chains with type guards can narrow types: if(shape.type === 'circle') { // circle } else if(shape.type === 'square') { // square } else { assertNever(shape); // catches missing cases } But if you forget the else or write conditions incorrectly, you lose exhaustiveness.
Result
Proper type narrowing helps maintain safe exhaustive checks.
Knowing how narrowing works helps you write safer conditional code that TypeScript can verify fully.
7
ExpertAdvanced tricks with exhaustive checks
🤔Before reading on: do you think you can enforce exhaustiveness without a default case in switch? Commit to yes or no.
Concept: You can enforce exhaustiveness by omitting default and enabling strict compiler options, or by using helper functions and type assertions.
With TypeScript's strict settings, if you omit the default case in a switch over a discriminated union, the compiler warns if cases are missing. Also, helper functions like 'assertNever' improve error messages. Example: switch(shape.type) { case 'circle': // handle break; case 'square': // handle break; // no default } This triggers errors if a new shape is added but not handled. You can also use exhaustiveCheck functions that accept only 'never' to catch missing cases in complex scenarios.
Result
Your code becomes future-proof and safer as TypeScript enforces completeness without runtime overhead.
Understanding compiler options and helper patterns lets you write truly exhaustive pattern matching that scales with your codebase.
Under the Hood
TypeScript uses its type system to track all possible types in a union. When you write a switch or if-else chain, it narrows the variable's type based on conditions. The 'never' type represents impossible states, so assigning a value to 'never' signals the compiler that some cases are unhandled. This triggers compile-time errors, preventing missing cases. The compiler's control flow analysis and strict settings work together to enforce exhaustiveness.
Why designed this way?
Exhaustive pattern matching was designed to catch bugs early by leveraging TypeScript's static type system. Instead of relying on runtime checks, it uses compile-time guarantees to ensure all cases are handled. This approach balances safety and performance, avoiding extra runtime code. Alternatives like runtime type checks are slower and error-prone, so static exhaustiveness is preferred.
Value (union type)
  │
  ├─ TypeScript narrows type based on condition
  │    ├─ Case 1 handled
  │    ├─ Case 2 handled
  │    └─ Missing case leads to 'never' assignment
  │         └─ Compile-time error
  └─ Result: safe, exhaustive handling guaranteed
Myth Busters - 4 Common Misconceptions
Quick: Does a switch statement in TypeScript always force you to handle all union cases? Commit to yes or no.
Common Belief:Switch statements automatically check all cases in a union type.
Tap to reveal reality
Reality:Switch statements do not enforce exhaustiveness unless you add explicit checks like a 'never' default case or use strict compiler options.
Why it matters:Assuming switch is exhaustive can cause missed cases and bugs that only appear at runtime.
Quick: Can you rely on runtime errors to catch unhandled cases safely? Commit to yes or no.
Common Belief:It's fine to handle only some cases and let runtime errors catch the rest.
Tap to reveal reality
Reality:Relying on runtime errors is unsafe and leads to crashes or unpredictable behavior; compile-time exhaustiveness is safer and more reliable.
Why it matters:Ignoring compile-time checks increases bugs and maintenance costs.
Quick: Does adding a default case in a switch always improve exhaustiveness? Commit to yes or no.
Common Belief:Adding a default case ensures all cases are handled.
Tap to reveal reality
Reality:A default case can hide missing cases by catching them silently; using 'never' in default or omitting default with strict checks is better.
Why it matters:Misusing default can mask bugs and reduce code clarity.
Quick: Is exhaustive pattern matching only useful for simple types like strings? Commit to yes or no.
Common Belief:Exhaustive matching is only for simple unions like strings or numbers.
Tap to reveal reality
Reality:It is especially powerful for complex discriminated unions with objects, enabling safe handling of rich data structures.
Why it matters:Underestimating its power limits code safety and expressiveness in real-world applications.
Expert Zone
1
TypeScript's exhaustiveness checks depend heavily on compiler strictness settings; turning off strictNullChecks or noImplicitReturns weakens guarantees.
2
Using helper functions like 'assertNever' improves error messages and debugging compared to relying on default case errors alone.
3
Exhaustive pattern matching can be combined with advanced type features like mapped types and conditional types for powerful, type-safe APIs.
When NOT to use
Exhaustive pattern matching is less useful when dealing with dynamic data from untyped sources or when performance constraints forbid extra checks. In such cases, runtime validation libraries or schema validation tools like io-ts or zod are better alternatives.
Production Patterns
In production, exhaustive pattern matching is used in state management (e.g., Redux reducers), parsing complex data formats, and handling API responses with discriminated unions. Teams enforce it via lint rules and strict compiler settings to prevent regressions.
Connections
Algebraic Data Types (ADTs)
Exhaustive pattern matching is the practical application of ADTs in TypeScript.
Understanding ADTs from functional programming helps grasp why exhaustive matching guarantees safety and completeness.
Formal Logic - Case Analysis
Exhaustive pattern matching mirrors the logical principle of case analysis, where all possibilities are considered.
Knowing this connection shows how programming safety relies on fundamental logical reasoning.
Medical Diagnosis Process
Both involve systematically checking all possible conditions to avoid missing critical cases.
Seeing pattern matching like a doctor ruling out all diseases helps appreciate its role in preventing errors.
Common Pitfalls
#1Missing cases in switch without error
Wrong approach:switch(value) { case 'red': // handle break; case 'green': // handle break; // missing 'blue' case }
Correct approach:switch(value) { case 'red': // handle break; case 'green': // handle break; case 'blue': // handle break; default: assertNever(value); }
Root cause:Assuming switch enforces exhaustiveness without explicit checks.
#2Using default case without 'never' check hides bugs
Wrong approach:switch(value) { case 'red': // handle break; default: console.log('default case'); }
Correct approach:switch(value) { case 'red': // handle break; default: assertNever(value); }
Root cause:Default case catches all unhandled cases silently, masking missing branches.
#3Ignoring compiler strictness settings
Wrong approach:// tsconfig.json with strict: false // code relies on exhaustiveness but compiler doesn't check
Correct approach:// tsconfig.json with strict: true // code uses assertNever and no default case
Root cause:Compiler settings control exhaustiveness checks; disabling strict mode weakens guarantees.
Key Takeaways
Exhaustive pattern matching ensures your code handles every possible case of a value, preventing bugs.
TypeScript's 'never' type and strict compiler options help enforce exhaustiveness at compile time.
Discriminated unions let you safely pattern match complex data structures with clear, maintainable code.
Default cases can hide missing branches unless combined with 'never' checks or omitted with strict settings.
Understanding and using exhaustive pattern matching improves code safety, readability, and future-proofing.