M

I

C

H

A

E

L

G

R

A

H

A

M


Understanding and Using Type Guards
Understanding and Using Type Guards

Mastering TypeScript: Understanding and Using Type Guards

As TypeScript has grown in popularity, one of its standout features is the way it enhances JavaScript by providing static typing. This not only improves code maintainability but also allows developers to catch errors early. However, dealing with complex types, unions, and narrowing types dynamically requires advanced techniques, and that's where Type Guards come into play.

In this blog, we’ll dive deep into what Type Guards are, how they work, and why they are essential when working with advanced types in TypeScript.


What Are Type Guards?

A Type Guard is a runtime check that ensures a variable or expression has a certain type. With Type Guards, you can "narrow" the type of a variable within a specific block of code. TypeScript uses these guards to refine types when certain conditions are met, helping to avoid type errors and giving you the benefit of intellisense.

TypeScript offers several ways to create these guards, including:

  1. typeof checks
  1. instanceof checks
  1. Custom Type Guards

Let’s explore each in detail.


1. Using typeof for Primitive Types

The typeof operator is commonly used to check the type of a variable at runtime. This is useful when dealing with basic data types such as strings, numbers, or booleans.

Example:

function isString(value: any): boolean {
  return typeof value === 'string';
}

function example(value: string | number) {
  if (isString(value)) {
    // TypeScript knows value is a string here
    console.log(value.toUpperCase());
  } else {
    // TypeScript knows value is a number here
    console.log(value.toFixed(2));
  }
}

In the above example, typeof narrows the union type string | number into either string or number, depending on the result of the check.


2. Using instanceof for Object Types

When working with objects, you can use the instanceof operator to check if an object is an instance of a class. This is particularly useful when dealing with classes or constructor functions.

Example:

class Dog {
  bark() {
    console.log('Woof!');
  }
}

class Cat {
  meow() {
    console.log('Meow!');
  }
}

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark(); // TypeScript knows animal is a Dog here
  } else {
    animal.meow(); // TypeScript knows animal is a Cat here
  }
}

Here, instanceof helps TypeScript understand whether the animal is a Dog or a Cat, so it can allow access to the appropriate method (bark or meow).


3. Custom Type Guards

For more complex use cases, you can create custom Type Guards using the is keyword. This technique is powerful for working with union types or interfaces, allowing you to define specific logic that checks whether a variable matches a particular type.

Example:

interface Car {
  drive: () => void;
  speed: number;
}

interface Boat {
  sail: () => void;
  knots: number;
}

function isCar(vehicle: Car | Boat): vehicle is Car {
  return (vehicle as Car).drive !== undefined;
}

function move(vehicle: Car | Boat) {
  if (isCar(vehicle)) {
    vehicle.drive(); // TypeScript knows vehicle is a Car here
    console.log(`Driving at ${vehicle.speed} mph`);
  } else {
    vehicle.sail(); // TypeScript knows vehicle is a Boat here
    console.log(`Sailing at ${vehicle.knots} knots`);
  }
}

In this example, isCar acts as a custom Type Guard, checking if the vehicle object has a drive method (indicating it's a Car). If the check passes, TypeScript narrows the type, allowing safe access to the Car's properties and methods.


4. Type Guards with in Operator

The in operator can also be used to create Type Guards, particularly when checking for the existence of a property in an object.

Example:

interface Bird {
  fly: () => void;
  wingspan: number;
}

interface Fish {
  swim: () => void;
  fins: number;
}

function moveAnimal(animal: Bird | Fish) {
  if ('fly' in animal) {
    animal.fly(); // TypeScript knows animal is a Bird here
    console.log(`Flying with a wingspan of ${animal.wingspan}`);
  } else {
    animal.swim(); // TypeScript knows animal is a Fish here
    console.log(`Swimming with ${animal.fins} fins`);
  }
}

Here, using the in operator helps TypeScript determine whether animal has a fly method, and thus whether it's a Bird or Fish.


5. Discriminated Unions

One of TypeScript's most powerful features for managing complex types is Discriminated Unions. These allow you to use a common property to differentiate between various types in a union.

Example:

interface Circle {
  kind: 'circle';
  radius: number;
}

interface Rectangle {
  kind: 'rectangle';
  width: number;
  height: number;
}

interface Triangle {
  kind: 'triangle';
  base: number;
  height: number;
}

type Shape = Circle | Rectangle | Triangle;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return (shape.base * shape.height) / 2;
    default:
      // Exhaustive check
      const _exhaustive: never = shape;
      throw new Error(`Unhandled case: ${_exhaustive}`);
  }
}

In this example, the kind property is used to distinguish between different shapes in the Shape union. TypeScript automatically narrows the type based on the value of kind, ensuring that only valid properties for the corresponding shape are accessed.


Why Use Type Guards?

Type Guards are essential in TypeScript for several reasons:

  • Safety: They ensure you're working with the correct type, preventing runtime errors.
  • Precision: Type Guards allow TypeScript to narrow down union types, giving you more precise control over variable behavior within specific code blocks.
  • Readability: They make your code easier to understand by clearly stating the conditions under which a type will be used.
  • Intellisense: Type Guards unlock better support from your IDE, as TypeScript can provide autocomplete and type suggestions based on the narrowed type.

Conclusion

Type Guards are a critical feature in TypeScript, enabling you to write safer, more reliable code, especially when working with union types or complex object structures. Whether you're checking primitives with typeof, classes with instanceof, or creating custom type guards, these techniques will help you build more robust applications.

Mastering Type Guards can take your TypeScript skills to the next level, allowing you to confidently handle even the most complex type scenarios. Happy coding!