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.
_18interface Dog {_18 type: "dog";_18 bark: () => void;_18}_18_18interface Cat {_18 type: "cat";_18 meow: () => void;_18}_18_18interface Bird {_18 type: "bird";_18 fly: () => void;_18}_18_18function 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
_12type Dog = { breed: string; bark: () => void };_12type Cat = { breed: string; meow: () => void };_12_12function isDog(pet: Dog | Cat): pet is Dog {_12 return (pet as Dog).bark !== undefined;_12}_12_12const pet: Dog | Cat = { breed: "Labrador", bark: () => console.log("Woof!") };_12_12if (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.
Tip
Interested to understand more about datatypes? Look no further! Mastering Data Types in JavaScript
The typeof
Guard
#The typeof
operator is useful for distinguishing primitive types like strings, numbers, and booleans.
_10function formatInput(input: string | number) {_10 if (typeof input === "string") {_10 return input.toUpperCase();_10 } else {_10 return input.toFixed(2);_10 }_10}_10_10console.log(formatInput("hello")); // "HELLO"_10console.log(formatInput(3.14159)); // "3.14"
The instanceof
Guard
#The instanceof
operator is useful when working with classes.
_22class Dog {_22 bark() {_22 console.log("Woof!");_22 }_22}_22_22class Cat {_22 meow() {_22 console.log("Meow!");_22 }_22}_22_22function makeSound(animal: Dog | Cat) {_22 if (animal instanceof Dog) {_22 animal.bark();_22 } else {_22 animal.meow();_22 }_22}_22_22makeSound(new Dog()); // "Woof!"_22makeSound(new Cat()); // "Meow!"
The in
Operator Guard
#The in
operator checks if an object contains a specific property.
_10type Dog = { bark: () => void };_10type Cat = { meow: () => void };_10_10function 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.
_18function 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_18const pet: unknown = { name: "Buddy", breed: "Golden Retriever" };_18_18if (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.
_13type Dog = { type: "dog"; bark: () => void };_13type Cat = { type: "cat"; meow: () => void };_13_13function 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_10export function isDog(pet: any): pet is Dog {_10 return pet && typeof pet.bark === "function";_10}_10_10export 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.
_10function hasProperty<T extends object, K extends keyof T>(obj: T, key: K): obj is T {_10 return key in obj;_10}_10_10const pet = { breed: "Labrador", bark: () => console.log("Woof!") };_10_10if (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 Method | Best Used For |
---|---|
typeof | Primitive types (string, number, boolean) |
instanceof | Class-based objects |
in operator | Checking object properties |
Custom is Type Guard | More complex type differentiation |
Discriminated Unions | Clean and scalable type checking |
By leveraging these techniques, you can create robust TypeScript applications that handle multiple pet types seamlessly.
Halpy coding,
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.
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?

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.