Cover image for Real-World Applications of Type Guards in TypeScript
User avatar
Posted on

Real-World Applications of Type Guards in TypeScript

In the world of TypeScript, precision and reliability are paramount. This is where Type Guards step onto the stage. Beyond theoretical discussions, this article ventures into practical terrain, demonstrating the tangible impact of Type Guards in real-world scenarios. Through carefully chosen examples, we'll explore how these constructs fortify your code against unexpected behavior. For those interested in a deeper theoretical exploration, our previous article - Demystifying User-Defined Type Guards in TypeScript - provides an insightful foundation.


Scenario 1: Ensuring Data Integrity - Validating Input Types from External Sources

In practical coding scenarios, we often encounter situations where data from external sources, like APIs, may not align perfectly with our expectations. One such case arises when handling vehicle data, where we receive a string claiming to represent a vehicle type. How can we be certain that it's a valid input?

Here's a code snippet illustrating this scenario:

const VehicleTypeValues = ['car', 'motorbike', 'trailer', 'caravan', 'transporter', 'truck'] as const;
type VehicleType = typeof VehicleTypeValues[number];

const isSupportedVehicleType = (input?: string): input is VehicleType => 
  VehicleTypeValues.some((value) => value === input);

// Usage Example:
const apiResponse: string | undefined = 'car';

if (isSupportedVehicleType(apiResponse)) {
  // apiResponse is now of type VehicleType
  console.log(`The received vehicle type "${apiResponse}" is valid.`);
} else {
  console.log(`The received vehicle type "${apiResponse}" is not supported.`);
}

We first define a set of supported vehicle types using a constant array. Then, we create a type guard function, isSupportedVehicleType, which takes an optional string input and returns a boolean indicating whether the input is a valid vehicle type. This function employs VehicleTypeValues.some to check if the input matches any of the supported vehicle types. If a match is found, it returns true, indicating that the input is indeed a valid vehicle type. From this point on, you are safe to assume that input is of type VehicleType.


Scenario 2: Validating Paths - Confirming String Integrity with JSON Keys

In web development, it's often crucial to verify that a given string corresponds to a key within a JSON object. This scenario arises when dealing with dynamic page paths in web applications.

type Page = {
  url: string;
  content: string;
  isActive: boolean;
};

type Pages = {
  [path: string]: Page;
};

const isValidPath = (path: string): path is keyof typeof jsonData => {
  return path in jsonData;
};

// Usage Example:
const jsonData: Pages = {
  '/article1': {
    url: '/article1',
    content: 'This is the content of article 1',
    isActive: true,
  },
  '/article2': {
    url: '/article2',
    content: 'This is the content of article 2',
    isActive: true,
  },
  '/about': {
    url: '/about',
    content: 'This is the about page',
    isActive: true,
  },
};

const userInput1 = '/article1';
const userInput2 = '/abc';

console.log(isValidPath(userInput1)); // Output: true
console.log(isValidPath(userInput2)); // Output: false

The provided code snippet showcases an example where we're managing a collection of pages represented by the Page type. These pages are stored in an object called jsonData, with each page identified by a unique path. The isValidPath function acts as a type guard, ensuring that a given input path is a valid key within jsonData. In the usage example, we test two inputs: userInput1 with the path '/article1' (which exists in jsonData) and userInput2 with the path '/abc' (which does not exist). The function correctly identifies the validity of these paths, providing reliable type narrowing.


Scenario 3: User Authentication Management - Distinguishing Authenticated and Guest Users

In most applications, managing user authentication is a fundamental aspect. This scenario tackles the challenge of distinguishing between authenticated users and guests using type guards.

interface AuthenticatedUser {
  username: string;
  token: string;
}

interface GuestUser {
  isGuest: true;
}

type User = AuthenticatedUser | GuestUser;

function isAuthenticatedUser(user: User): user is AuthenticatedUser {
  return 'token' in user;
}

// Usage Example:
const authenticatedUser: AuthenticatedUser = {
  username: 'john_doe',
  token: 'xyz123',
};

const guestUser: GuestUser = {
  isGuest: true,
};

console.log(isAuthenticatedUser(authenticatedUser)); // Output: true
console.log(isAuthenticatedUser(guestUser)); // Output: false

In this code snippet, we define two interfaces, AuthenticatedUser and GuestUser, representing authenticated and guest users respectively. The User type is a union of both interfaces. The isAuthenticatedUser function acts as a type guard, verifying whether a given user is authenticated. It is a type predicate function that checks if the provided user object contains the property token. If it does, TypeScript will narrow down the type to AuthenticatedUser.


Scenario 4: Navigating API Responses - Distinguishing Success from Errors

Managing API responses, especially those that may include error payloads, is a common challenge. This scenario demonstrates how type guards can be a powerful tool to discern between successful and error responses.

interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
}

function handleApiResponse<T>(response: ApiResponse<T>) {
  if (response.success) {
    // Handle successful response
    console.log('Success! Data:', response.data);
  } else if (response.error) {
    // Handle error response
    console.error('Error:', response.error);
  }
}

// Usage Example:
const successResponse: ApiResponse<number> = {
  success: true,
  data: 42,
};

const errorResponse: ApiResponse<number> = {
  success: false,
  error: 'Internal Server Error',
};

handleApiResponse(successResponse); // Output: Success! Data: 42
handleApiResponse(errorResponse); // Output: Error: Internal Server Error

In this code snippet, we start by defining an ApiResponse<T> interface, which can represent either successful or error responses. The handleApiResponse function, which takes an ApiResponse<T> as an argument, employs a type guard to differentiate between the two types of responses. If response.success is true, it's treated as a successful response, and the data is logged to the console. On the other hand, if response.error exists, indicating an error response, the error message is logged. This code structure allows for effective management of API responses, enhancing code reliability in real-world scenarios.


Embrace Type Guards: Elevate Your Code's Precision and Reliability

We've just scratched the surface of what type guards can actually do. Now it's your time to shine! Incorporate these techniques into your projects and make your code run with precision and reliability. Say goodbye to unexpected surprises and hello to a more confident and efficient development journey.