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
}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}`);
}
}Exam tip
Custom 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.
Think you're ready? Prove it.
Take the free TypeScript readiness test. Get a score, topic breakdown, and your exact weak areas.
Take the free TypeScript test →Free · No sign-up · Instant results