Know if you're actually ready. Take the TypeScript quiz → get your AI readiness report.Take the free test →
Study guide · TypeScript
TypeScript Type Narrowing and Type Guards
TypeScript narrows union types inside conditional blocks. Here's all the narrowing patterns and custom type guards.
Built-in narrowing
TypeScript narrows types automatically inside standard conditional checks.
function process(value: string | number | null) {
// typeof narrows primitives
if (typeof value === "string") {
return value.toUpperCase(); // string
}
if (typeof value === "number") {
return value.toFixed(2); // number
}
// value is null here
return "no value";
}
// Truthiness narrows null/undefined/falsy
function display(name: string | null) {
if (name) {
console.log(name.toUpperCase()); // string — null is excluded
}
}instanceof and in narrowing
instanceof narrows class instances. The in operator narrows object shapes.
class NetworkError extends Error {
status: number;
constructor(status: number) {
super();
this.status = status;
}
}
function handleError(error: Error | NetworkError) {
if (error instanceof NetworkError) {
console.log(error.status); // NetworkError properties available
}
}
// in operator
type Cat = { meow(): void; purr(): void };
type Dog = { bark(): void; fetch(): void };
function interact(animal: Cat | Dog) {
if ("meow" in animal) {
animal.meow(); // animal is Cat
} else {
animal.bark(); // animal is Dog
}
}Custom type guard functions
Type guard functions use value is Type as their return type to help TypeScript narrow types.
// Type predicate: "value is User" tells TypeScript to narrow
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value
);
}
function processInput(data: unknown) {
if (isUser(data)) {
console.log(data.name); // TypeScript knows data is User
}
}
// Useful for validating API responses
async function getUser(id: string) {
const data = await fetch(`/api/users/${id}`).then(r => r.json());
if (!isUser(data)) throw new Error("Invalid user data");
return data; // type: User
}NoteCustom type guards are essential for validating data at runtime boundaries (API responses, user input, localStorage).
The never type and exhaustive checks
The never type represents impossible values. It's used to ensure all union members are handled.
type Shape = "circle" | "square" | "triangle";
function getArea(shape: Shape): number {
switch (shape) {
case "circle": return Math.PI * 5 ** 2;
case "square": return 5 ** 2;
case "triangle": return (5 * 4) / 2;
default:
const exhaustiveCheck: never = shape;
// If you add a new Shape without handling it here,
// TypeScript will show an error at compile time
throw new Error(`Unhandled shape: ${exhaustiveCheck}`);
}
}NoteThe exhaustive never check is a powerful pattern — TypeScript will give you a compile error if you add a new union member and forget to handle it.
Exam tipCustom type guards (value is Type) are commonly asked in senior TypeScript interviews. Know that without them, TypeScript can't narrow types based on your own validation logic — the predicate syntax is what tells the compiler about the narrowing.
Want a person to help?
Struggling with TypeScript? Work 1:1 with a vetted TypeScript tutor on Wyzant. (affiliate link)
Find a TypeScript tutor →