jude hunter

jude hunter

The caveats of generic type guards in TypeScript

thumbnail

When dealing with user data - especially in APIs - and inheritance, it’s often difficult to generalize your code and follow the DRY principle.

The TypeScript language uses a concept called type guards 🛡️ - a clever compiler feature that will help you write safer code and deal with that angry and complaining compiler.

The compiler uses guards to narrow down the type of your value and provide IntelliSense suggestions.

Say we have a given inheritance model:

class Vehicle {
  brand: string;
}

class Aircraft extends Vehicle {
  usage: 'civil' | 'military';
}

class Car extends Vehicle {
  drive: 'AWD' | 'FWD' | 'RWD';
}

We are given a secretVehicle object that we know extends Vehicle in terms of the properties it has. However, the object is not an instance of any of these classes.

Thus, the instanceof approach won’t work, since it requires the left operand to be an instance:

if (secretVehicle instanceof Car) {
  console.log(`
    This is a car with ${secretVehicle.drive} drive
  `);
  // TypeScript doesn't complain,
  // but this will never print!
}

What we can do instead, is check if our secretVehicle has all the properties of our subclasses.

We do that by either using reflection or by creating an actual instance of that class and looking up its keys with Object.keys():

export const hasAllKeys =
  <T>(obj: Record<string, any>, cls: new () => T)
  : obj is T => {
    const properties = Object.keys(new cls());
    for (const p of properties) {
      if (!(p in obj)) return false;
    }
    return true;
  };

We can then use the guard to assure TypeScript that the secretVehicle is actually of a given type.

if (hasAllKeys(secretVehicle, Car)) {
  console.log(`
    This is a car with ${secretVehicle.drive} drive
  `);
}
if (hasAllKeys(secretVehicle, Aircraft)) {
  console.log(`
    This is a ${secretVehicle.usage} aircraft
  `);
}

However, in some edge cases this solution is problematic. It may incorrectly check the properties when used with a class that has a custom constructor.

Moreover, sometimes it’s simply not what we need. The input data we get is often just a Partial<T> instead of a T, meaning some properties might be missing (e.g. the id).

To counter that, let’s use a guard that checks for specific properties instead of all of them.

export const hasKeys = <T>(
  obj: Record<string, any>,
  properties: (keyof T)[]
): obj is T =>
  properties
    .filter((p) => p in obj)
    .length == properties.length;
    // functional approach

The TypeScript compiler is clever enough to figure out T by itself, if we don’t want to specify it.

For instance, hasKeys(secretVehicle, ['usage']) will infer T to be of type {usage: any} and thus, we will be able to use the usage key inside of our if statement.

if (hasKeys(secretVehicle, ['usage'])) {
  console.log(`
    Not sure what this is,
    but it has a ${secretVehicle.usage} usage!
  `);
}

Alas, this forces us to operate on values of type any. We can either pass the the type for that key:

hasKeys<{usage: 'civil' | 'military'}>(
  secretVehicle,
  ['usage'],
);

Or just simply pass the entire class:

hasKeys<Aircraft>(secretVehicle, ['usage']);

This will also give us IntelliSense suggestions when defining the keys!

Still, what if both our subclasses have the same fields, but of different types? The issue gets more complicated and may require the usage of reflection.

However, we can overcome this problem by specifying a type field in our base class to easily differentiate between types.

class Vehicle {
  brand: string;
  type: 'Car' | 'Aircraft';
}

const ofType =
  <T>(
    obj: Record<string, any> & {type ?: string},
    cls: new () => T
  ): obj is T =>
    obj.type == (new cls()).constructor.name;
    // or use another argument for the type field

    if (ofType(secretVehicle, Car)) {
      console.log(`
        This is a car with ${secretVehicle.drive} drive
      `);
}

TypeScript is a powerful language and using these constructs can help you use it to its full potential.

Happy coding! 🎉


Thanks for reading.
HI, YOU’VE REACHED
jude hunter
OPEN TO WORK
Front-end engineer, advocate of free-as-in-freedom software and an active social progressivist
LEAVE A MESSAGE AT THE TONE
get in contact
made with ❤️ and 🏳️‍🌈
by jude hunter ©
legal name: Mateusz Sowinski-Smetny