Suggest an editImprove this articleRefine the answer for “When does type narrowing happen?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Type narrowing** happens when TypeScript gets **enough information** to **rule out some variants** from a union or unspecified type. **Key point:** it kicks in on `typeof`, `instanceof`, `in` checks, `===`/`!==` comparisons, truthy/falsy checks, custom type guards, union discriminants, and control-flow analysis (`return`, `throw`), and once the branch ends TypeScript merges the possible types back together.Shown above the full answer for quick recall.Answer (EN)Image## The key idea > **Type narrowing** happens > when TypeScript gets **enough information** to **rule out some variants** from a union or unspecified type. That is: - We have a variable with a "wide" type (`string | number | null | undefined | ...`), - Some **logical condition or check** occurs, - TypeScript concludes: "Given this condition holds, only these variants of the type remain". --- ## When exactly narrowing happens TypeScript **automatically narrows the type** on the following events: | Category | Mechanism | Example | |---|---|---| | **1. Checks via operators** | `typeof`, `instanceof`, `in` | `if (typeof x === "string")` | | **2. Equality / inequality checks** | `===`, `!==`, `==`, `!=` | `if (x === null)` | | **3. Truthy / falsy checks** | `if (x)`, `!x`, `x && y`, `x | | | **4. Custom type guards** | `value is Type` | `if (isString(val))` | | **5. Discriminant checks (discriminated unions)** | `if (obj.kind === "circle")` or `switch (obj.status)` | | | **6. Control flow** | `return`, `throw`, `break`, `continue` | after `return` the remaining types are excluded | | **7. Logical expressions with types** | `!x`, `x && y`, `x ? a : b` | each branch has its own narrowed type | | **8. Using** `never` **after full exclusion** | when TS "understands" no other types remain | `default:` in a `switch` after all cases | --- ## Let's look at each category with examples --- ### 1. `typeof`, `instanceof`, `in` ```javascript function example(value: string | number | boolean) { if (typeof value === "string") { // value: string } else if (typeof value === "number") { // value: number } else { // value: boolean } } ``` ```javascript if (x instanceof Date) { // x: Date } ``` ```javascript if ("length" in obj) { // obj: { length: number } or similar } ``` --- ### 2. Equality checks ```javascript function greet(name: string | null) { if (name === null) { console.log("No name"); } else { // name: string console.log("Hello, " + name.toUpperCase()); } } ``` After `if (name === null)`, TS **excludes** `null` from the union in the `else` branch. --- ### 3. Truthy / Falsy checks ```javascript function process(user?: { name: string } | null) { if (!user) { // user: null | undefined return; } // Here user: { name: string } console.log(user.name); } ``` Any "emptiness" check (`if (x)`) automatically removes `null` and `undefined`. --- ### 4. Custom Type Guards ```javascript function isString(value: unknown): value is string { return typeof value === "string"; } function print(value: unknown) { if (isString(value)) { // value: string console.log(value.toUpperCase()); } } ``` TS trusts the `isString` function - if it returns `true`, then inside that branch the type of `value` is definitely `string`. --- ### 5. Discriminated unions ```javascript type Shape = | { kind: "circle"; radius: number } | { kind: "square"; side: number }; function area(shape: Shape) { if (shape.kind === "circle") { // shape: { kind: "circle"; radius: number } return Math.PI * shape.radius ** 2; } else { // shape: { kind: "square"; side: number } return shape.side ** 2; } } ``` TS **narrows the type by the discriminant** (`kind`). --- ### 6. Control flow ```javascript function example(x: string | null | undefined) { if (x == null) return; // excludes null and undefined // Here x: string console.log(x.toUpperCase()); } ``` TypeScript sees that after `return` the "branch" with `null`/`undefined` is cut off, and only the remaining variants continue. --- ### 7. Logical expressions ```javascript function showName(user?: { name: string } | null) { // user?.name narrows the type inside const name = user && user.name; // user: { name: string } | null | undefined if (name) { // name: string console.log(name.toUpperCase()); } } ``` --- ### 8. Narrowing to `never` ```javascript function handle(val: "a" | "b") { switch (val) { case "a": return "Got A"; case "b": return "Got B"; default: const _exhaustive: never = val; // TS guarantees this is unreachable return _exhaustive; } } ``` TypeScript understands that after handling all possible cases, the type has narrowed to `never` - no variants remain. --- ## When narrowing *does not* happen 1. If the type is too wide (`any`), TS does not narrow it: ```javascript let value: any; if (typeof value === "string") { // value: any - TS knows nothing } ``` 2. If the check does not give unambiguous information: ```javascript let v: string | number; if (v) { // still string | number } ``` 3. If TS cannot track control flow (for example, when a variable is mutated). --- ## At what point in compilation this happens TypeScript performs **control flow analysis (CFA)** - an analysis of the program's execution path, step by step: 1. It scans every instruction (`if`, `return`, `throw`, `try`, `switch`); 2. It builds a "type flow graph"; 3. In every branch it stores the "current type of the variable"; 4. After leaving a block, it merges the possible types back together (union merge). --- ### Example: how TypeScript thinks ```javascript let x: string | number | null; if (x === null) { // x: null } else if (typeof x === "string") { // x: string } else { // x: number } ``` Internally, TS does roughly this: ```javascript Initial type: string | number | null -> if (x === null): branch 1 = null -> else: branch 2 = string | number -> if (typeof x === "string"): branch 2a = string -> else: branch 2b = number ``` --- ## Summary | Question | Answer | |---|---| | **When does narrowing happen?** | When TypeScript sees a logical check that excludes part of the possible types | | **What exactly does it do?** | It "drops" types that are impossible in the current branch | | **Based on what?** | `typeof`, `instanceof`, `in`, `===`, `==`, `if (x)`, `return`, `throw`, `type guards`, discriminants | | **When does it stop?** | After leaving the branch, TS merges all possible types back together | | **Key feature** | It is *automatic control flow analysis (CFA)* built into the compiler |For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.