Now Reading:

The Definitive Guide To Narrowing Types Using Type Guards

TheType guards in TypeScript are like a well-trained guard dog—they protect your code by ensuring variables are the types you expect before you use them. But as your codebase grows, maintaining and updating type guards can become a challenge.

How do you keep your type guards scalable and maintainable? What happens when your data models evolve? In this article, we'll dive into best practices for managing type guards in TypeScript, ensuring they remain reliable and easy to update as your project scales.

Why Type Guards Matter

#

Consider a scenario where we have a function that takes a pet as an argument. The pet could be a dog, a cat, or a bird, and we want to handle them differently.


_18
interface Dog {
_18
type: "dog";
_18
bark: () => void;
_18
}
_18
_18
interface Cat {
_18
type: "cat";
_18
meow: () => void;
_18
}
_18
_18
interface Bird {
_18
type: "bird";
_18
fly: () => void;
_18
}
_18
_18
function interactWithPet(pet: Dog | Cat | Bird) {
_18
pet.bark(); // ERROR: Property 'bark' does not exist on type 'Cat | Bird'.
_18
}

Of course we know birds and cats don’t bark (yet) in this case; TypeScript throws an error because bark() is not available on Cat or Bird. How can we prevent this kind of confusion? This is where Type Guards come in to ensure type safety.

What Are Type Guards?

#

A type guard is a function or expression that refines a value to a more specific type. TypeScript uses these guards to ensure that operations on a variable are safe.

Basic Type Guard


_12
type Dog = { breed: string; bark: () => void };
_12
type Cat = { breed: string; meow: () => void };
_12
_12
function isDog(pet: Dog | Cat): pet is Dog {
_12
return (pet as Dog).bark !== undefined;
_12
}
_12
_12
const pet: Dog | Cat = { breed: "Labrador", bark: () => console.log("Woof!") };
_12
_12
if (isDog(pet)) {
_12
pet.bark(); // TypeScript knows pet is a Dog here
_12
}

Here, isDog ensures that pet is a Dog before calling bark(). This is a great way to ensure that if the pet is a dog we can ask it to bark. This is all well in good but lets explore some more ways we can type guard based on data types.

The typeof Guard

#

The typeof operator is useful for distinguishing primitive types like strings, numbers, and booleans.


_10
function formatInput(input: string | number) {
_10
if (typeof input === "string") {
_10
return input.toUpperCase();
_10
} else {
_10
return input.toFixed(2);
_10
}
_10
}
_10
_10
console.log(formatInput("hello")); // "HELLO"
_10
console.log(formatInput(3.14159)); // "3.14"

The instanceof Guard

#

The instanceof operator is useful when working with classes.


_22
class Dog {
_22
bark() {
_22
console.log("Woof!");
_22
}
_22
}
_22
_22
class Cat {
_22
meow() {
_22
console.log("Meow!");
_22
}
_22
}
_22
_22
function makeSound(animal: Dog | Cat) {
_22
if (animal instanceof Dog) {
_22
animal.bark();
_22
} else {
_22
animal.meow();
_22
}
_22
}
_22
_22
makeSound(new Dog()); // "Woof!"
_22
makeSound(new Cat()); // "Meow!"

The in Operator Guard

#

The in operator checks if an object contains a specific property.


_10
type Dog = { bark: () => void };
_10
type Cat = { meow: () => void };
_10
_10
function makeNoise(animal: Dog | Cat) {
_10
if ("bark" in animal) {
_10
animal.bark();
_10
} else {
_10
animal.meow();
_10
}
_10
}

Custom Type Predicates With The is Keyword

#

A more structured way to write type guards is using custom type predicates with the is keyword. The is keyword is used to define a custom type predicate in a function. This function acts as a type guard, allowing TypeScript to narrow down the type of a variable inside a conditional check.


_18
function isDog(value: unknown): value is Dog {
_18
return (
_18
typeof value === "object" &&
_18
value !== null &&
_18
"name" in value &&
_18
"breed" in value &&
_18
typeof (value as Dog).name === "string" &&
_18
typeof (value as Dog).breed === "string"
_18
);
_18
}
_18
_18
const pet: unknown = { name: "Buddy", breed: "Golden Retriever" };
_18
_18
if (isDog(pet)) {
_18
console.log(`${pet.name} is a ${pet.breed}.`); // ✅ Safe access
_18
} else {
_18
console.log("Not a valid dog object.");
_18
}

Discriminated Unions Are Best Practice

#

A discriminated union uses a common literal property to differentiate between types. This is often the best approach when designing TypeScript types.


_13
type Dog = { type: "dog"; bark: () => void };
_13
type Cat = { type: "cat"; meow: () => void };
_13
_13
function makeNoise(animal: Dog | Cat) {
_13
switch (animal.type) {
_13
case "dog":
_13
animal.bark();
_13
break;
_13
case "cat":
_13
animal.meow();
_13
break;
_13
}
_13
}

Best Practices For Maintaining Type Guards

#

Keep Them In A Centralised Place

Instead of scattering type guards throughout your codebase, centralise them in a single file for example typeGuards.ts


_10
// typeGuards.ts
_10
export function isDog(pet: any): pet is Dog {
_10
return pet && typeof pet.bark === "function";
_10
}
_10
_10
export function isCat(pet: any): pet is Cat {
_10
return pet && typeof pet.meow === "function";
_10
}

Now, any part of your code that needs a type guard can import it from typeGuards.ts.

Automate Type Guard Updates With Generics

If your project frequently introduces new types, you can use generics to create reusable type guards.


_10
function hasProperty<T extends object, K extends keyof T>(obj: T, key: K): obj is T {
_10
return key in obj;
_10
}
_10
_10
const pet = { breed: "Labrador", bark: () => console.log("Woof!") };
_10
_10
if (hasProperty(pet, "bark")) {
_10
pet.bark(); // Safe to call
_10
}

This method keeps type guards flexible and easy to maintain.

Conclusion

#

Type Guards in TypeScript provide a safe and effective way to distinguish between different types. Whether you're using typeof, instanceof, in operator, custom type guards, or discriminated unions, they all help ensure type safety and avoid runtime errors. To understand the best case for your situation you can reference this handy table.

Type Guard MethodBest Used For
typeofPrimitive types (string, number, boolean)
instanceofClass-based objects
in operatorChecking object properties
Custom is Type GuardMore complex type differentiation
Discriminated UnionsClean and scalable type checking

By leveraging these techniques, you can create robust TypeScript applications that handle multiple pet types seamlessly.

Halpy coding,

DANNY

Quiz Time

#

Reinforce Your Learning

One of the best ways to reinforce what your learning is to test yourself to solidify the knowlege in your memory.
Complete this 10 question quiz to see how much you remember.

1) What is the main purpose of a type guard in TypeScript?
file under:
Typescript

Thanks alot for your feedback!

The insights you share really help me with improving the quality of the content here.

If there's anything you would like to add, please send a message to:

[email protected]

Was this article this helpful?

D is for danny

About the author

Danny Engineering

A software engineer with a strong belief in human-centric design and driven by a deep empathy for users. Combining the latest technology with human values to build a better, more connected world.

Gobacktothetop

Made with 🥰 in 🏴󠁧󠁢󠁥󠁮󠁧󠁿

©2025 All rights reserved.