Skip to main content

What is type narrowing in TypeScript?

What is Type Narrowing

Type narrowing is the process by which TypeScript refines a variable's type based on logical checks, operators, and the flow of the code.

In simpler terms: TypeScript "watches" your if, typeof, in, instanceof, and other checks, and figures out which type is actually possible in each branch of the code.


Example: without narrowing

javascript
function printId(id: string | number) { console.log(id.toUpperCase()); // Error - id can be a number }

id has the type string | number, and TypeScript does not know which one is currently in use.


Example with narrowing

javascript
function printId(id: string | number) { if (typeof id === "string") { // Here id: string console.log(id.toUpperCase()); } else { // Here id: number console.log(id.toFixed(2)); } }

TypeScript has narrowed the type of id: string in the first branch, number in the second.


How narrowing works

TypeScript performs control flow analysis and automatically "cuts off" impossible types when it encounters:

  • checks with typeof, instanceof, in, ===, !==, ==, !=;
  • checks against null / undefined;
  • custom type guards (value is Type);
  • logical statements (if, else, switch, return, throw, etc.).

The main ways to narrow types

MethodDescriptionExample
typeofFor primitivestypeof x === "string"
instanceofFor classes and constructorsx instanceof Date
inChecks whether a property exists"prop" in obj
=== / !==Comparison of literal valuesx === null
null / undefined checksExcludes "empty" valuesif (x)
Custom type guardA function of the form x is TisArray(value): value is any[]

Examples of different narrowing variants

1. typeof - primitives

javascript
function log(value: string | number | boolean) { if (typeof value === "string") { value.toUpperCase(); // string } else if (typeof value === "number") { value.toFixed(2); // number } else { value.valueOf(); // boolean } }

2. instanceof - classes and objects

javascript
function logDate(d: Date | string) { if (d instanceof Date) { d.toISOString(); // Date } else { d.toUpperCase(); // string } }

3. in - checking whether a property exists

javascript
type Dog = { bark: () => void }; type Cat = { meow: () => void }; function speak(animal: Dog | Cat) { if ("bark" in animal) { animal.bark(); // Dog } else { animal.meow(); // Cat } }

4. == null checks

javascript
function greet(name?: string | null) { if (name == null) { console.log("No name provided"); } else { console.log("Hello, " + name.toUpperCase()); } }

After if (name == null) TypeScript narrows the type to null | undefined, and after else, to string.


5. With a custom type guard

javascript
function isString(value: unknown): value is string { return typeof value === "string"; } function print(value: unknown) { if (isString(value)) { value.toUpperCase(); // now definitely a string } }

Custom type guards are a way to teach TS to "recognize" your own types.


6. With literal types and comparisons

javascript
type Status = "loading" | "success" | "error"; function handle(status: Status) { if (status === "loading") { // status: "loading" } else if (status === "success") { // status: "success" } else { // status: "error" } }

This is a special case of discriminated union narrowing (narrowing by a discriminant property).


Narrowing in union types (Union Narrowing)

If a variable has the type A | B | C, TypeScript will narrow it to a specific subtype once a condition rules out the rest.

javascript
function example(x: string | number | boolean) { if (typeof x === "string") { // string } else if (typeof x === "number") { // number } else { // boolean } }

Narrowing to never

When TypeScript rules out every possible option, the variable gets the type never (an unreachable type).

javascript
function process(value: string | number) { if (typeof value === "string") { return value.toUpperCase(); } else if (typeof value === "number") { return value.toFixed(2); } else { // value: never (this branch cannot be reached) throw new Error("Unexpected type"); } }

Why this matters

Safety: TS will not let you call a method that does not belong to the type.

Convenience: The IDE suggests the available properties and methods after narrowing.

Clear code: You can write clean if/switch statements without unnecessary casts (as).

The basis of discriminated unions: Without narrowing, TS would not be able to tell which variant of a union type is in use.


Summary

ConceptDescription
Type narrowingThe process of refining a variable's type based on conditions
Why it is neededSo TypeScript knows which subtype is actually in use at a given point
When it triggersOn checks (typeof, instanceof, in, ===, is, null, etc.)
ResultTS "narrows" a union or unknown type down to a specific one
The "never" typeUsed when no other option remains

Just remember:

Type narrowing is when TypeScript "thinks like a human": it reads your if and figures out what type the value has inside each branch.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.