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.
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#
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", ">> ")}]`);[ 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:
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(""));Hello, ASHA | Hello, guest | Hello, guestBe 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:
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));OK no value 3.00value == 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:
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" }));swimming flyinginstanceof#
For classes (including built-ins like Date and Error), instanceof narrows:
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 "));2026-10-24 24 OctArray.isArray#
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]));5 6Control flow analysis#
TypeScript tracks types through the whole flow of a function, including early returns, else branches, switch statements and assignments:
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));0 42 7Narrowing 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:
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" }));[ '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
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));null
2026-01-01T00:00:00.000Z
a,b
yes
3.00
"hi"Key takeaway#
Finished this lesson?