User Defined Type Guards

Photo by RetroSupply on Unsplash

User Defined Type Guards

Β·

4 min read

Imagine that you have a transportation array. Our array will have car and plane objects in it. Our task will be to add a type to our array and print car emoji or a plane emoji in our UI. Let's create an array and add types for it.

interface Car {
  hasWings: boolean;
  name: string;
  color: string;
  wheels: number;
}
interface Plane {
  hasWings: boolean;
  color: string;
  name: string;
  isJet: boolean;
}
const transportationList: (Plane | Car)[] = [
  { hasWings: false, name: "car", color: "red", wheels: 4 },
  { hasWings: true, name: "plane", color: "blue", isJet: true }
];

Great, we got our transportation list that will be an array with a union type of Plane or Car. Think of a union as a sum of two types. It can have everything from Plane or Car or both.

Alright, let's move to the fun part and show something on the screen. Since we know the car can not have wings (well, maybe one day. That would be so much fun!), we can check hasWings property to decide what to show in UI:

    <div className="App">
      {transportationList.map((el) => {
        if (!el.hasWings) {
          return (
            <div>
              This is a {el.name} <span>πŸš•</span>{" "}
            </div>
          );
        }

        return (
          <div>
            This is a plane <span>✈️</span> {el.isJet}
          </div>
        );
      })}
    </div>

Oh no, it looks like TS is unhappy with the type we provided for our transportation list. πŸ₯² Property 'isJet' does not exist on type 'Car | Plane'. Property 'isJet' does not exist on type 'Car'. Okay, we can add type assertion to tell TypeScript that this is a Plane. Our code will look like this.

This is a plane <span>✈️</span> {(el as Plane).isJet}

The final code will look like this

interface Car {
  hasWings: boolean;
  name: string;
  color: string;
  wheels: number;
}

interface Plane {
  hasWings: boolean;
  color: string;
  name: string;
  isJet: boolean;
}

export default function App() {
  const transportationList: (Plane | Car)[] = [
    { hasWings: false, name: "car", color: "red", wheels: 4 },
    { hasWings: true, name: "plane", color: "blue", isJet: true }
  ];

  return (
    <div className="App">
      {transportationList.map((el) => {
        if (!el.hasWings) {
          return (
            <div>
              This is a {el.name} <span>πŸš•</span>
            </div>
          );
        }

        return (
          <div>
            This is a plane <span>✈</span> {(el as Plane).isJet}
          </div>
        );
      })}
    </div>
  );
}

It seems to fix our problem, but imagine we have more things to show on the screen for our plane. We will have to add type assertions to each one, and our code will no longer follow the DRY principle.

What if we can tell TS what type it should be based on hasWings property? πŸ€” User-Defined Type Guards to rescue.

Let's refactor our code a little bit. First, create a union type called Transport and change transportationList to use this new type.

type Transport = Car | Plane;

 const transportationList: Transport[] = [
    { hasWings: false, name: "car", color: "red", wheels: 4 },
    { hasWings: true, name: "plane", color: "blue", isJet: true }
  ];

Now let's build a function that accepts a single parameter and returns a boolean, and when that boolean is true, the argument we passed into a function is a Plane.

const isPlane = (val: Transport): val is Plane => !!val.hasWings;

The is **operator is what lets us define the type. In UI let's change if (!el.hasWings) to if(isPlane(el))

<div className="App">
{transportationList.map((el) => {
        if (!isPlane(el)) {
          return (
            <div>
              This is a {el.name} <span>πŸš•</span>
            </div>
          );
        }
        return (
          <div>
            This is a plane <span>✈️</span> {el.isJet}
          </div>
        );
      })}
</div>

To see TS magic hover over the el.isJet, you will see that TS knows that the element is a Plane type and will no longer throw an error as it did before. 🎊

You might not use user-defined type guards very often, but if you will they are great for keeping your code DRY and clean.

And here is our final code:

interface Car {
  hasWings: boolean;
  name: string;
  color: string;
  wheels: number;
}

interface Plane {
  hasWings: boolean;
  color: string;
  name: string;
  isJet: boolean;
}

type Transport = Car | Plane;

const isPlane = (val: Transport): val is Plane => !!val.hasWings;

export default function App() {
  const transportationList: Transport[] = [
    { hasWings: false, name: "car", color: "red", wheels: 4 },
    { hasWings: true, name: "plane", color: "blue", isJet: true }
  ];


  return (
    <div className="App">
      {transportationList.map((el) => {
        if (!isPlane(el)) {
          return (
            <div>
              This is a {el.name} <span>πŸš•</span>
            </div>
          );
        }
        return (
          <div>
            This is a plane <span>✈️</span> {el.isJet}
          </div>
        );
      })}
    </div>
  );
}

Happy coding! πŸ‘©πŸΌβ€πŸ’»

(p.s. please excuse any typos. English is my second language)

Β