JavaScript > TypeScript > Advanced TypeScript > Type guards

TypeScript Type Guards: Narrowing Types for Safer Code

Explore TypeScript type guards to refine variable types within conditional blocks, ensuring type safety and enabling specific operations based on type checks. This guide offers practical examples and best practices for effective type narrowing.

Understanding Type Guards

Type guards are TypeScript functions that narrow down the type of a variable within a conditional block. They enable the TypeScript compiler to understand more precisely what type a variable holds, leading to more accurate type checking and fewer errors.

Without type guards, TypeScript might not allow you to perform certain operations on a variable because it can't be sure of its type. Type guards provide that certainty.

Basic Type Guard Example: 'typeof' Operator

This example uses the typeof operator as a type guard. Inside the if block, TypeScript infers that input is a string, allowing you to access its length property. In the else block, TypeScript knows it's a number, so you can use toFixed().

function printLength(input: string | number) {
  if (typeof input === 'string') {
    // Within this block, TypeScript knows 'input' is a string
    console.log(input.length);
  } else {
    // Within this block, TypeScript knows 'input' is a number
    console.log(input.toFixed(2));
  }
}

Custom Type Guards: Using Type Predicates

Here, we define a custom type guard function isBird. The return type pet is Bird is a type predicate. If isBird(pet) returns true, TypeScript knows that pet is of type Bird within the if block. If it returns false, TypeScript knows it is a Fish.

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

function isBird(pet: Bird | Fish): pet is Bird {
  return (pet as Bird).fly !== undefined;
}

function careForPet(pet: Bird | Fish) {
  if (isBird(pet)) {
    pet.fly();
  } else {
    pet.swim();
  }
}

Using the 'in' Operator as a Type Guard

The in operator checks if a property exists on an object. In this example, if 'radius' in shape returns true, TypeScript infers that shape is a Circle. Otherwise, it's a Square.

interface Circle {
  radius: number;
}

interface Square {
  sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape) {
  if ('radius' in shape) {
    return Math.PI * shape.radius * shape.radius; // TypeScript knows 'shape' is a Circle
  } else {
    return shape.sideLength * shape.sideLength; // TypeScript knows 'shape' is a Square
  }
}

Real-Life Use Case: Handling API Responses

Type guards are useful when dealing with API responses that can be either successful or erroneous. Checking the success property allows you to narrow down the type and handle the response accordingly.

interface SuccessResponse {
  success: true;
  data: any;
}

interface ErrorResponse {
  success: false;
  error: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
  if (response.success) {
    console.log('Data:', response.data);
  } else {
    console.error('Error:', response.error);
  }
}

Best Practices

  • Keep type guards simple and focused: Each type guard should have a clear purpose and only narrow down the type based on a single criteria.
  • Use descriptive names for type guard functions: The name should clearly indicate what type is being checked (e.g., isString, isNumber, isUser).
  • Consider using discriminated unions: For more complex scenarios, discriminated unions can often provide a cleaner and more type-safe alternative to complex type guard logic.

Interview Tip

When discussing type guards in an interview, be prepared to explain the purpose of type narrowing, how type guards achieve this, and the different techniques for implementing them (typeof, instanceof, custom type predicates, and the in operator). Also, be prepared to discuss the trade-offs of using type guards versus other type-checking techniques.

When to Use Them

Use type guards when you need to perform operations that are specific to certain types within a union. They are particularly useful when dealing with:

  • Functions that accept union types as arguments.
  • API responses that can have different structures based on success or failure.
  • Complex object structures where you need to differentiate between types based on property existence.

Alternatives

Alternatives to type guards include:

  • Discriminated unions: Defining a union type with a common, discriminating property that allows you to easily identify the underlying type.
  • Overloads: Providing multiple function signatures with different parameter types and return types.
  • Assertion functions: Functions that throw an error if a certain condition is not met, effectively narrowing the type.

Pros

  • Improved Type Safety: They ensure that you are only performing operations that are valid for the current type of a variable.
  • Enhanced Code Readability: They make your code more explicit and easier to understand by clearly indicating type checks.
  • Reduced Runtime Errors: By catching type-related errors at compile-time, they help prevent runtime errors.

Cons

  • Increased Code Complexity: Implementing type guards can add complexity to your code, especially for complex type unions.
  • Maintenance Overhead: As your types evolve, you may need to update your type guards accordingly.

FAQ

  • What happens if I don't use a type guard when working with a union type?

    TypeScript might prevent you from performing certain operations on the variable because it cannot be sure of its type. You might encounter type errors at compile time.
  • Can I use type guards with classes?

    Yes, you can use the instanceof operator as a type guard to check if an object is an instance of a particular class.
  • Are type guards a runtime or compile-time feature?

    Type guards primarily affect TypeScript's compile-time type checking. While the type guard functions themselves are executed at runtime, their main purpose is to provide type information to the compiler.