TypeScript Type Guards
When working with TypeScript, you often need to handle values that could be of multiple types. Type guards are special expressions that help TypeScript understand the type of a variable within a certain scope, typically within conditional blocks. This powerful feature allows you to write type-safe code that can adapt to different data types at runtime.
What Are Type Guards?
Type guards are expressions that perform a runtime check to guarantee the type of a value in a specific context. They allow TypeScript to narrow down the type of an object within a conditional block.
function printLength(value: string | string[]): void {
if (Array.isArray(value)) {
// TypeScript knows value is string[]
console.log(`Array length: ${value.length}`);
} else {
// TypeScript knows value is string
console.log(`String length: ${value.length}`);
}
}
// Input
printLength("Hello");
printLength(["Hello", "World"]);
// Output
// String length: 5
// Array length: 2
In this example, Array.isArray()
serves as a type guard that helps TypeScript determine whether value
is a string or an array within each code block.
Built-in Type Guards in TypeScript
TypeScript offers several built-in type guards to check types at runtime:
1. typeof
Type Guard
The typeof
operator checks the primitive type of a value:
function formatValue(value: string | number): string {
if (typeof value === "string") {
// TypeScript knows value is a string here
return value.toUpperCase();
} else {
// TypeScript knows value is a number here
return value.toFixed(2);
}
}
// Input
console.log(formatValue("hello"));
console.log(formatValue(42.1234));
// Output
// HELLO
// 42.12
The typeof
type guard works well for JavaScript primitive types: string
, number
, boolean
, undefined
, object
, function
, bigint
, and symbol
.
2. instanceof
Type Guard
The instanceof
operator checks if an object is an instance of a class:
class Dog {
bark() {
return "Woof!";
}
}
class Cat {
meow() {
return "Meow!";
}
}
function makeSound(animal: Dog | Cat): string {
if (animal instanceof Dog) {
// TypeScript knows animal is Dog here
return animal.bark();
} else {
// TypeScript knows animal is Cat here
return animal.meow();
}
}
// Input
const myDog = new Dog();
const myCat = new Cat();
console.log(makeSound(myDog));
console.log(makeSound(myCat));
// Output
// Woof!
// Meow!
3. Property Checking Type Guard
TypeScript can narrow types by checking if a specific property exists:
interface Bird {
fly(): void;
name: string;
}
interface Fish {
swim(): void;
name: string;
}
function move(pet: Bird | Fish): void {
if ("fly" in pet) {
// TypeScript knows pet is Bird here
pet.fly();
} else {
// TypeScript knows pet is Fish here
pet.swim();
}
}
// Example implementation
const myBird: Bird = {
name: "Sparrow",
fly() {
console.log(`${this.name} is flying`);
}
};
const myFish: Fish = {
name: "Nemo",
swim() {
console.log(`${this.name} is swimming`);
}
};
// Input
move(myBird);
move(myFish);
// Output
// Sparrow is flying
// Nemo is swimming
Custom Type Guards
For more complex scenarios, you can create your own custom type guards using type predicates:
interface Car {
make: string;
model: string;
year: number;
}
interface Smartphone {
brand: string;
model: string;
releaseYear: number;
}
// Define a custom type guard
function isCar(item: Car | Smartphone): item is Car {
return (item as Car).make !== undefined;
}
function printDetails(item: Car | Smartphone): void {
if (isCar(item)) {
// TypeScript knows item is Car here
console.log(`Car: ${item.make} ${item.model}, ${item.year}`);
} else {
// TypeScript knows item is Smartphone here
console.log(`Phone: ${item.brand} ${item.model}, ${item.releaseYear}`);
}
}
// Input
const myCar: Car = {
make: "Toyota",
model: "Corolla",
year: 2020
};
const myPhone: Smartphone = {
brand: "Apple",
model: "iPhone 13",
releaseYear: 2021
};
printDetails(myCar);
printDetails(myPhone);
// Output
// Car: Toyota Corolla, 2020
// Phone: Apple iPhone 13, 2021
The key part is the item is Car
return type annotation. This is a type predicate that tells TypeScript that if the function returns true
, the provided argument is of the specified type.
Discriminated Unions
A discriminated union (also called a tagged union) is a powerful pattern that works well with type guards:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
// TypeScript knows shape is Circle here
return Math.PI * shape.radius ** 2;
case "square":
// TypeScript knows shape is Square here
return shape.sideLength ** 2;
}
}
// Input
const circle: Circle = {
kind: "circle",
radius: 5
};
const square: Square = {
kind: "square",
sideLength: 4
};
console.log(calculateArea(circle));
console.log(calculateArea(square));
// Output
// 78.53981633974483
// 16
The kind
property serves as a discriminant or tag, allowing TypeScript to determine which type we're working with in each case.
Real-world Example: API Response Handling
Type guards are especially useful when working with APIs that might return different response structures:
interface SuccessResponse {
status: "success";
data: {
id: number;
name: string;
email: string;
};
}
interface ErrorResponse {
status: "error";
error: {
code: number;
message: string;
};
}
type ApiResponse = SuccessResponse | ErrorResponse;
function handleResponse(response: ApiResponse): void {
if (response.status === "success") {
// TypeScript knows this is a SuccessResponse
const { id, name, email } = response.data;
console.log(`User ${name} (ID: ${id}) has email: ${email}`);
} else {
// TypeScript knows this is an ErrorResponse
const { code, message } = response.error;
console.error(`Error ${code}: ${message}`);
}
}
// Input example
const successResponse: ApiResponse = {
status: "success",
data: {
id: 1,
name: "John Doe",
email: "[email protected]"
}
};
const errorResponse: ApiResponse = {
status: "error",
error: {
code: 404,
message: "User not found"
}
};
handleResponse(successResponse);
handleResponse(errorResponse);
// Output
// User John Doe (ID: 1) has email: [email protected]
// Error 404: User not found
Type Guards With null
and undefined
Type guards can be useful for handling null
and undefined
values, which is a common requirement in real applications:
function processValue(value: string | null | undefined): string {
// Simple null check as a type guard
if (value === null) {
return "Value is null";
}
// Simple undefined check as a type guard
if (value === undefined) {
return "Value is undefined";
}
// At this point, TypeScript knows value is string
return value.toUpperCase();
}
// Input
console.log(processValue("hello"));
console.log(processValue(null));
console.log(processValue(undefined));
// Output
// HELLO
// Value is null
// Value is undefined
You can also use non-null assertion operator (!
) when you're certain a value isn't null or undefined, but use it with caution:
function getFirstElement(arr: string[] | null): string {
// Type guard first
if (arr !== null && arr.length > 0) {
return arr[0];
}
throw new Error("Array is empty or null");
}
Using Type Guards with Generic Types
Type guards can also be used with generic types:
function isArray<T>(value: T | T[]): value is T[] {
return Array.isArray(value);
}
function processItems<T>(items: T | T[]): T[] {
if (isArray<T>(items)) {
// TypeScript knows items is T[]
return items;
} else {
// TypeScript knows items is just T
return [items];
}
}
// Input
console.log(processItems("hello"));
console.log(processItems(["hello", "world"]));
// Output
// ["hello"]
// ["hello", "world"]
When to Use Type Guards
Type guards are particularly useful in the following scenarios:
- When working with union types
- When handling API responses that might have different formats
- When dealing with functions that accept multiple parameter types
- When processing data from external sources with uncertain types
- When implementing robust error handling
- When working with libraries that have loosely typed interfaces
Summary
Type guards are a fundamental feature in TypeScript that bridge the gap between compile-time type checking and runtime type determination. They allow you to write type-safe code that adapts to different data types at runtime, making your code more robust and easier to maintain.
Key points to remember about type guards:
- They perform runtime checks to narrow down types within conditional blocks
- Built-in type guards include
typeof
,instanceof
, and property checks - Custom type guards use the
is
type predicate syntax - Discriminated unions provide a clean pattern for handling different object shapes
- Type guards are essential for handling real-world situations like API responses
Exercises
- Create a type guard to differentiate between an array of numbers and an array of strings.
- Implement a function that can process either a Date object or an ISO date string.
- Build a discriminated union for different shapes (triangle, rectangle, circle) with methods to calculate both area and perimeter.
- Create a custom type guard that can identify if an object implements a specific interface.
- Build an error handling system for an API client that uses type guards to handle different error responses.
Additional Resources
- TypeScript Handbook: Narrowing
- TypeScript Playground - Experiment with type guards
- Type Predicates in the TypeScript documentation
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)