Suggest an editImprove this articleRefine the answer for “What does "control flow based narrowing" mean?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Control flow-based narrowing** is a TypeScript mechanism that **analyzes the program's control flow** and **automatically narrows the type of variables** based on logical conditions, checks, and code branches. **Key point:** TypeScript tracks which checks you perform and understands which types remain possible at every point in the program.Shown above the full answer for quick recall.Answer (EN)Image## What Control Flow Based Narrowing Is > **Control flow-based narrowing** is a TypeScript mechanism > that **analyzes the program's control flow** > and **automatically narrows the type of variables** based on logical conditions, checks, and code branches. Simply put: > TypeScript tracks **which checks you perform**, > and "understands" **which types remain possible** at every point in the program. --- ## Example 1. A basic example - `if` ```javascript function printId(id: string | number) { if (typeof id === "string") { // Here TS sees: id is definitely string console.log(id.toUpperCase()); } else { // And here: id is definitely number console.log(id.toFixed(2)); } } ``` TypeScript analyzes the **control flow (**`if`**/**`else`**)**, and narrowed the type of `id` in each branch. --- ## "Control flow" is the path the program takes at runtime Internally, TypeScript builds a **control flow graph (CFG)**: - every `if`, `else`, `return`, `throw`, `switch`, `try/catch` branch is a separate "branch" of the flow; - in each branch, variables can have different types; - when flows return to a common block, TS "merges" the types back together. --- ### Visually: ```javascript let x: string | number; if (typeof x === "string") { // branch 1 -> x: string } else { // branch 2 -> x: number } // after the if -> x: string | number (merged) ``` --- ## Example 2. Null and undefined checks ```javascript function greet(name?: string | null) { if (!name) return; // filters out null and undefined // Here name: string console.log("Hello, " + name.toUpperCase()); } ``` TypeScript tracks that after `if (!name) return`, the possible types `null` and `undefined` are **excluded** from the union. --- ## Example 3. Narrowing through `return` and `throw` TypeScript understands that if execution **is interrupted**, then the remaining branch has a different type. ```javascript function ensureString(value: string | undefined): string { if (value === undefined) { throw new Error("Expected string"); } // Here value: string (undefined excluded) return value.toUpperCase(); } ``` TS knows: "if the `throw` did not run, then `value` is definitely not undefined". --- ## Example 4. Narrowing through `in`, `instanceof`, `typeof` ```javascript type Cat = { meow: () => void }; type Dog = { bark: () => void }; function speak(pet: Cat | Dog) { if ("bark" in pet) { // pet: Dog pet.bark(); } else { // pet: Cat pet.meow(); } } ``` TypeScript uses a **property check** as logical proof, to exclude a type that does not match. --- ## Example 5. A combined narrowing flow ```javascript function example(value: string | number | null) { if (value == null) { // value: null return; } if (typeof value === "string") { // value: string console.log(value.toUpperCase()); } else { // value: number console.log(value.toFixed(2)); } } ``` TS analyzes the control flow **step by step**: ```javascript starting type: string | number | null -> after if (value == null): null excluded -> after if (typeof value === "string"): number excluded ``` --- ## Example 6. Narrowing on assignment ```javascript let x: string | number = 42; if (typeof x === "number") { x = x + 1; // x: number } else { x = x.toUpperCase(); // x: string } ``` TypeScript even tracks **how types change after an assignment**, which is also part of *control flow analysis*. --- ## Example 7. Narrowing inside ternary operators ```javascript function format(value: string | number) { return typeof value === "string" ? value.toUpperCase() : value.toFixed(2); } ``` TS narrows the types on each branch of the ternary expression. --- ## Example 8. Combined with `never` TypeScript can "understand" that **all variants have been handled**, and the remaining type becomes `never`. ```javascript type Shape = | { kind: "circle"; r: number } | { kind: "square"; s: number }; function area(shape: Shape) { switch (shape.kind) { case "circle": return Math.PI * shape.r ** 2; case "square": return shape.s ** 2; default: const _exhaustive: never = shape; // TS checks that this is unreachable return _exhaustive; } } ``` This is an example of **discriminated narrowing**, based on flow analysis (`switch`/`case`). --- ## How TypeScript does this "under the hood" 1. **It builds a control flow graph:** - every branching point (`if`, `return`, `throw`, `catch`) creates a node. 2. **It assigns possible types to every node.** 3. **It tracks exceptions and returns**, where a branch terminates. 4. **It merges types back together** when flows converge. This is what is called **control flow analysis**: analyzing types *in the context of the program's execution*. --- ## Example 9. TS understands even complex expressions ```javascript function test(a?: number, b?: number) { if (a && b) { // a: number, b: number } else if (a) { // a: number, b: undefined } else { // a: undefined, b: number | undefined } } ``` TS computes the types *in every branch* depending on which checks succeeded. --- ## Example 10. Narrowing after a `try/catch` check ```javascript function safeParse(input: string): object | null { try { return JSON.parse(input); } catch { return null; } } ``` TS knows: - inside `try`, the result is `object`, - inside `catch`, it is `null`. This is also control flow. --- ## Why this matters Without "control flow based narrowing", TypeScript would just be a "type validator" with no understanding of the program's logic. But with it: - the compiler **analyzes real branches and checks**; - types are automatically **narrowed** wherever it is logically justified; - the IDE shows **context-aware hints**; - you can **stop being afraid** of `null`**,** `undefined`**,** `union` **and** `unknown` - TS protects you from errors at the level of the execution flow. --- ## Summary | Term | Meaning | |---|---| | **Control flow** | The program's execution path: `if`, `else`, `return`, `throw`, `switch`, `try` | | **Narrowing** | Narrowing the set of possible types down to a specific one | | **Control flow-based narrowing** | When TS automatically narrows types by analyzing the code's logic and branches | | **Main idea** | A variable's type changes depending on what has been "proven" in the current branch | | **Result** | Less `as`, fewer errors, more safety and better autocomplete |For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.