Skip to content
Laviyx Learn

Try

TypeScript course menu

TypeScript Narrowing: typeof, instanceof, in, Equality and Truthiness Checks

Learn how TypeScript narrows union types using control flow: typeof guards, truthiness checks, equality checks, the in operator, instanceof, Array.isArray, early returns, and how narrowing flows through if, switch and logical operators.

Intermediate8 min readUpdated
On this page

When a value has a union type, you usually need to find out which member it is before using it. Narrowing is how TypeScript follows your checks: inside an if that tests typeof x === "string", it knows x is a string. This works with ordinary JavaScript checks; you do not need any special syntax.

typeof#

TypeScript
function padLeft(value: string, padding: string | number): string {
  if (typeof padding === "number") {
    return " ".repeat(padding) + value;     // padding: number
  }
  return padding + value;                   // padding: string
}

console.log(`[${padLeft("hi", 3)}]`);
console.log(`[${padLeft("hi", ">> ")}]`);
Output
[   hi]
[>> hi]

typeof checks for "string", "number", "boolean", "bigint", "symbol", "undefined", "object" and "function". Remember that typeof null is "object", so a typeof x === "object" check does not rule out null.

Truthiness#

Checking a value in an if removes null, undefined and other falsy values:

TypeScript
function greet(name: string | null | undefined): string {
  if (name) {
    return `Hello, ${name.toUpperCase()}`;   // name: string
  }
  return "Hello, guest";
}

console.log(greet("asha"), "|", greet(null), "|", greet(""));
Output
Hello, ASHA | Hello, guest | Hello, guest

Be careful: truthiness also treats "" and 0 as "missing", as the last call shows. When those are valid values, check against null explicitly (Truthy and Falsy).

Equality#

Comparing with === narrows to the types both sides could share:

TypeScript
function compare(a: string | number, b: string | boolean): string {
  if (a === b) {
    return a.toUpperCase();   // both must be strings here
  }
  return "different";
}

function describe(value: number | null): string {
  if (value === null) return "no value";
  return value.toFixed(2);    // value: number
}

console.log(compare("ok", "ok"), describe(null), describe(3));
Output
OK no value 3.00

value == null (two equals signs) narrows away both null and undefined.

The in operator#

"prop" in obj narrows to the union members that have that property:

TypeScript
type Fish = { swim: () => string };
type Bird = { fly: () => string };

function move(animal: Fish | Bird): string {
  if ("swim" in animal) {
    return animal.swim();
  }
  return animal.fly();
}

console.log(move({ swim: () => "swimming" }), move({ fly: () => "flying" }));
Output
swimming flying

instanceof#

For classes (including built-ins like Date and Error), instanceof narrows:

TypeScript
function formatDate(value: Date | string): string {
  if (value instanceof Date) {
    return value.toISOString().slice(0, 10);   // value: Date
  }
  return value.trim();                          // value: string
}

console.log(formatDate(new Date("2026-10-24T00:00:00Z")), formatDate("  24 Oct  "));
Output
2026-10-24 24 Oct

Array.isArray#

TypeScript
function total(input: number | number[]): number {
  if (Array.isArray(input)) {
    return input.reduce((a, b) => a + b, 0);
  }
  return input;
}

console.log(total(5), total([1, 2, 3]));
Output
5 6

Control flow analysis#

TypeScript tracks types through the whole flow of a function, including early returns, else branches, switch statements and assignments:

TypeScript
function parseAge(input: string | number | undefined): number {
  if (input === undefined) {
    return 0;                       // undefined handled: gone from here on
  }
  if (typeof input === "string") {
    input = Number.parseInt(input, 10);   // input becomes a number
  }
  return input;                     // input: number
}

console.log(parseAge(undefined), parseAge("42"), parseAge(7));
Output
0 42 7

Narrowing also works in &&, ||, ?: and ?? expressions, and in switch (typeof value) statements.

When narrowing is lost#

Narrowing applies to the code TypeScript can see between the check and the use. It is reset when the value could have changed, for example inside a callback that runs later:

TypeScript
type User = { name: string; nickname?: string };

function show(user: User): string[] {
  if (user.nickname) {
    const nickname = user.nickname;      // copy the narrowed value
    return ["a", "b"].map((x) => `${x}:${nickname.toUpperCase()}`);
  }
  return [];
}

console.log(show({ name: "Asha", nickname: "ash" }));
Output
[ 'a:ASH', 'b:ASH' ]

Copying the narrowed value into a const is a common, simple way to keep it narrowed inside callbacks.

For checks TypeScript cannot understand by itself, you can write your own narrowing functions; see Type Guards and Assertion Functions.

Practice#

Practice exercise

Write stringify(value: string | number | boolean | Date | null | string[]): string that returns: "null" for null, the ISO date for a Date, items joined with commas for arrays, "yes"/"no" for booleans, numbers with two decimals, and strings in quotes.

Show solution
TypeScript
function stringify(value: string | number | boolean | Date | null | string[]): string {
  if (value === null) return "null";
  if (value instanceof Date) return value.toISOString();
  if (Array.isArray(value)) return value.join(",");
  if (typeof value === "boolean") return value ? "yes" : "no";
  if (typeof value === "number") return value.toFixed(2);
  return `"${value}"`;
}

const samples = [null, new Date("2026-01-01T00:00:00Z"), ["a", "b"], true, 3, "hi"];
for (const s of samples) console.log(stringify(s));
Output
null
2026-01-01T00:00:00.000Z
a,b
yes
3.00
"hi"

Key takeaway#

Finished this lesson?