How to Use Type Guards in TypeScript

What you’ll build or solve

You’ll narrow union and unknown types using built-in and custom type guards.

When this approach works best

Type guards work best when you:

  • Handle union types like string | number or User | Admin
  • Validate API responses before accessing nested data
  • Build reusable utilities that accept multiple possible types

They also help when working with unknown, where you must check the type before using it.

This is a bad idea if you use guards to patch over poorly defined types. If your type definitions are wrong, fix them at the source.

Prerequisites

  • TypeScript installed
  • A .ts file
  • Basic understanding of union types and interfaces

Step-by-step instructions

Step 1: Use typeof for primitive types

Use typeof when narrowing primitive unions like string, number, or boolean.

functionformatValue(value:string|number) {
if (typeofvalue==="string") {
returnvalue.toUpperCase();
  }

returnvalue.toFixed(2);
}

Inside the if block, TypeScript treats value as string. In the else branch, it treats it as number.

Use this for primitive unions and for narrowing unknown values before accessing properties.

Step 2: Use instanceof for class instances

Use instanceof when checking objects created with classes.

classDog {
  bark() {
return"Woof";
  }
}

classCat {
  meow() {
return"Meow";
  }
}

functionspeak(animal:Dog|Cat) {
if (animalinstanceofDog) {
returnanimal.bark();
  }

returnanimal.meow();
}

After the instanceof check, TypeScript narrows animal to Dog inside that block.

This works only with classes, not interfaces or plain object types.

Step 3: Use the in operator for object properties

Use the in operator to check for a property that exists in one type but not another.

interfaceAdmin {
  role:string;
  permissions:string[];
}

interfaceUser {
  email:string;
}

functiongetInfo(person:Admin|User) {
if ("permissions"inperson) {
returnperson.permissions.join(", ");
  }

returnperson.email;
}

When "permissions" in person is true, TypeScript treats person as Admin.

This works well with interfaces and plain objects.

Step 4: Create custom type guards with type predicates

When built-in operators are not enough, define your own guard using a type predicate.

interfaceAdmin {
  role:string;
  permissions:string[];
}

interfaceUser {
  email:string;
}

functionisAdmin(person:Admin|User):personisAdmin {
return"permissions"inperson;
}

functiongetDashboard(person:Admin|User) {
if (isAdmin(person)) {
return`Admin panel with${person.permissions.length} permissions`;
  }

return`User dashboard for${person.email}`;
}

The return type person is Admin tells TypeScript that if the function returns true, person is an Admin.

Use custom guards when:

  • You repeat the same check in multiple places
  • The validation logic is more than a single condition
  • You want clearer, reusable narrowing logic

What to look for

  • Narrow unknown with typeof before accessing properties
  • Use guards before calling type-specific methods
  • Prefer guards over type assertions like as
  • Use custom guards for reusable or multi-condition checks

Examples you can copy

Example 1: Handling flexible input

functionprocessInput(input:string|number) {
if (typeofinput==="string") {
returninput.trim();
  }

returninput+10;
}

This safely handles both text and numeric input without casting.

Example 2: Discriminated union from an API

typeApiResponse=
| { success:true; data:string[] }
| { success:false; error:string };

functionhandleResponse(response:ApiResponse) {
if (response.success) {
returnresponse.data.join(", ");
  }

return`Error:${response.error}`;
}

Checking response.success narrows the union automatically. TypeScript understands each branch based on the literal boolean.

Example 3: Validating unknown data

interfaceProduct {
  id:number;
  price:number;
}

functionisProduct(value:unknown):valueisProduct {
return (
typeofvalue==="object"&&
value!==null&&
"id"invalue&&
"price"invalue
  );
}

functionprintProduct(value:unknown) {
if (isProduct(value)) {
console.log(`Product${value.id}: $${value.price}`);
  }else {
console.log("Invalid product");
  }
}

This pattern protects your code from runtime crashes when data does not match the expected shape.

Common mistakes and how to fix them

Mistake 1: Accessing properties without narrowing

You might write:

functiongetLength(value:string|number) {
returnvalue.length;
}

Why it breaks: number does not have a length property, so TypeScript throws an error.

Correct approach:

functiongetLength(value:string|number) {
if (typeofvalue==="string") {
returnvalue.length;
  }

returnvalue.toString().length;
}

Narrow first, then access type-specific properties.

Mistake 2: Using type assertions instead of guards

You might force a type:

functionprintEmail(user:Admin|User) {
return (userasUser).email;
}

Why it breaks: If user is actually an Admin, this can fail at runtime.

Correct approach:

functionprintEmail(user:Admin|User) {
if ("email"inuser) {
returnuser.email;
  }

return"No email available";
}

Prefer runtime checks over forced assertions.

Troubleshooting

  • If you see “Property does not exist on type”, add a guard before accessing that property.
  • If narrowing does not work inside a helper function, confirm it returns a type predicate like value is MyType.
  • If a custom guard always returns false, log the value and check property names carefully.
  • If instanceof fails, confirm the object was created with a class constructor and not as a plain object.
  • If you work with unknown and cannot access properties, narrow it first with typeof or a custom guard.

Quick recap

  • Use typeof for primitive types and unknown
  • Use instanceof for class instances
  • Use in for object property checks
  • Create reusable guards with value is Type
  • Narrow before accessing type-specific properties