Skip to content

Type Guards in TypeScript

Type guards are expressions that perform runtime checks to narrow a type within a scope. They tell TypeScript’s compiler: “in this block, this value is definitely this specific type.”

flowchart TB
Wide[Wide Type<br/>string | number | boolean] --> Check{Type Guard Check}
Check -->|typeof === 'string' ✅| Narrow1[Narrowed: string<br/>Can use .toUpperCase()]
Check -->|typeof === 'number' ✅| Narrow2[Narrowed: number<br/>Can use .toFixed()]
Check -->|typeof === 'boolean' ✅| Narrow3[Narrowed: boolean<br/>Can use ! negation]
style Wide fill:#7c3aed,color:#fff
style Check fill:#f59e0b,color:#fff
style Narrow1 fill:#3b82f6,color:#fff
style Narrow2 fill:#059669,color:#fff
style Narrow3 fill:#ec4899,color:#fff

Analogy: A wide type is like a box labeled “mixed items.” A type guard is like opening the box and checking each item — once you know it’s a hammer, you can use it as a hammer.

// typeof — for primitives
function process(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // value is string
}
return value.toFixed(2); // value is number
}
// instanceof — for classes
class Dog { bark() {} }
class Cat { meow() {} }
function makeSound(pet: Dog | Cat) {
if (pet instanceof Dog) {
pet.bark(); // pet is Dog
} else {
pet.meow(); // pet is Cat
}
}
// in — for property checking
interface Bird { fly(): void }
interface Fish { swim(): void }
function move(animal: Bird | Fish) {
if ("fly" in animal) {
animal.fly(); // animal is Bird
} else {
animal.swim(); // animal is Fish
}
}
// Custom type guard using "is"
interface User { name: string; email: string; role: "user" }
interface Admin { name: string; email: string; role: "admin"; permissions: string[] }
function isAdmin(user: User | Admin): user is Admin {
return user.role === "admin";
}
function getUserDashboard(user: User | Admin) {
if (isAdmin(user)) {
return user.permissions; // user is Admin
}
return ["read"]; // user is User
}
// API response type guard
type ApiResult<T> =
| { success: true; data: T }
| { success: false; error: string };
function isSuccess<T>(result: ApiResult<T>): result is { success: true; data: T } {
return result.success;
}
// Usage
const result = await fetchUser();
if (isSuccess(result)) {
console.log(result.data.name); // Safe access
} else {
console.error(result.error); // Error access
}

Easy: What is a type guard? Give an example with typeof.

Medium: How do you create a user-defined type guard?

Hard: When would you use the in operator as a type guard vs a custom type guard function?