How To Write a TypeScript Type Guard
Code Readability Game Changers

TypeScript is such a game changer. It is so powerful and amazing to use. It has the ability to make working on massive codebases with multitudes of developers a simple process. As projects grow in complexity, maintaining a clear understanding of data types and their properties becomes increasingly challenging. This is where TypeScript, with its robust type system, comes to the rescue. When moving into a new area of code TypeScript will guide you through complex functions by telling you all the data structures, and return types of the functions. It will tell when your making a mistake before you even run a compilation.
Among TypeScript’s arsenal of features, one particularly powerful tool stands out: the TypeScript Type Guard. I have worked with a lot of talented TypeScript developers I often find that the TypeGuard concept lags behind in understanding so that is why I wrote this walk through:
Understanding TypeScript Type Guards
TypeScript Type Guards are functions used to narrow down the type of a variable within a conditional block. These functions help TypeScript understand the specific type of an object or variable, enabling more precise type inference and facilitating safer code execution. While TypeScript’s type system already provides static type checking, Typeguards offer an additional layer of assurance, especially in scenarios involving union types or complex conditional logic.
Consider the following TypeScript example which works but is not using a typeguard :
enum CartTypes {
sale,
return,
}
interface Cart {
cartType: CartTypes;
total: number;
salePromotion?: string;
returnReason?: string;
}
interface SaleCart {
cartType: CartTypes.sale;
total: number;
salePromotion: string;
}
interface ReturnCart {
cartType: CartTypes.return;
total: number;
returnReason: string;
}
// Format number to US dollar
let USDollar = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
});
// our checkoutCart function is given carts of many types and it
// needs to figure out which checkout type to handle
function checkoutCart(cart: Cart): string {
if (cart.cartType === 0 && cart.salePromotion) {
return `${salePromotion} - ${USDollar.format(cart.total)}`;
} else if (cart.cartType === 1 && cart.returnReason) {
return `${returnReason} - ${USDollar.format(-cart.total)}`;
}
throw new Error('Unsupported cart');
}
// Example usage
const saleCart = { cartType: 0, total: 47.5, salePromotion: 'Holidy Sale' };
const returnCart = { cartType: 1, total: 15.97, returnReason: 'Item Damaged' };
console.log(checkoutCart(saleCart));
// Output: "Holiday Sale - $47.50"
console.log(checkoutCart(returnCart));
// Output: "Item Damaged -$15.97"In this example, the `checkoutCart` function accepts a `Cart` object and formats the total amount as a US dollar currency string with sale promotion string. However, within the function, we need to differentiate between a sale cart and a return cart to handle them appropriately. We achieve this differentiation using conditional statements based on the `cartType` property and other expected properties.
While this approach works, it can lead to code duplication and reduced readability, especially as the number of conditional blocks increases. This is where TypeScript Type Guards come into play.
Elevating Code Clarity with Advanced Type Guards
Let’s enhance the previous example by introducing more advanced Type Guards
function isSaleCart(cart: Cart): cart is SaleCart {
return cart.cartType === 0;
}
function isReturnCart(cart: Cart): cart is ReturnCart {
return cart.cartType === 1;
}
// our function does the same thing but is a bit more
// easy to read and easy to use
function checkoutCart(cart: Cart): string {
if (isSaleCart(cart)) {
// Here we've changed the type of cart to SaleCart and
// the cart will have the expected SaleCart types for example
// cart.salePromotion will always be a string
// and TypesScript would tell us cart.returnReason is a bad type
return `${salePromotion} - ${USDollar.format(cart.total)}`;
} else if (isReturnCart(cart)) {
// Here we've changed the type of cart to ReturnCart and
// the cart will have the expected ReturnCart types for example
// cart.returnReason will always be a string
// and TypesScript would tell us cart.salePromotion is a bad type
return `${returnReason} - ${USDollar.format(-cart.total)}`;
}
throw new Error('Unsupported cart');
}
In this updated version, we’ve defined two Type Guard functions: `isSaleCart` and `isReturnCart`. These functions accept a `Cart` object and return a boolean value indicating whether the object matches the specified cart type. By using these Type Guards within the `checkoutCart` function, we can confidently access properties specific to each cart type without resorting to manual type checks.
By leveraging TypeScript Type Guards, developers can enhance code clarity, reduce redundancy, and promote safer code execution. Type Guards empower developers to write cleaner, more maintainable code, ensuring that TypeScript’s powerful type system is fully utilized to its fullest potential. As projects scale and complexity grows, embracing TypeScript Type Guards is essential for building robust, type-safe applications in the ever-evolving landscape of software development.
If you enjoyed this content and would like to support me in these endeavors please visit: https://ko-fi.com/jacobmacinnis
