Skip to main content

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.

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

  1. When working with union types
  2. When handling API responses that might have different formats
  3. When dealing with functions that accept multiple parameter types
  4. When processing data from external sources with uncertain types
  5. When implementing robust error handling
  6. 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

  1. Create a type guard to differentiate between an array of numbers and an array of strings.
  2. Implement a function that can process either a Date object or an ISO date string.
  3. Build a discriminated union for different shapes (triangle, rectangle, circle) with methods to calculate both area and perimeter.
  4. Create a custom type guard that can identify if an object implements a specific interface.
  5. Build an error handling system for an API client that uses type guards to handle different error responses.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)