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:
Learn TypeScript on Mimo
- Handle union types like
string | numberorUser | 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
.tsfile - 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
unknownwithtypeofbefore 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:
PHP
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:
SQL
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
instanceoffails, confirm the object was created with a class constructor and not as a plain object. - If you work with
unknownand cannot access properties, narrow it first withtypeofor a custom guard.
Quick recap
- Use
typeoffor primitive types andunknown - Use
instanceoffor class instances - Use
infor object property checks - Create reusable guards with
value is Type - Narrow before accessing type-specific properties
Join 35M+ people learning for free on Mimo
4.8 out of 5 across 1M+ reviews
Check us out on Apple AppStore, Google Play Store, and Trustpilot