Execute Program

Advanced TypeScript: Keyof With Typeof

Welcome to the Keyof With Typeof lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • We saw keyof and typeof in earlier lessons. In this lesson, we'll see one way to use them together. The example here is taken from a simplified version of real code used in Execute Program.

  • Execute Program is a React application written in TypeScript. We use a React component called Icon to render all of the icons in the application, like the arrow icon in the "Continue" button below.

  • The details of React aren't important here, so it's OK if you haven't used it before. For this lesson, we only need to know that Icon is a function, and it takes objects like {name: 'rightArrow'} or {name: 'billing'}. The real component returns an SVG image for the browser to render, but the version we write here will return placeholder strings for simplicity.

  • Icon is a simple function, but we still have many choices when deciding on its type. The simplest and most obvious type is:

  • >
    function Icon(props: {name: string}) {
    ...
    }
  • That type works, but it allows many mistakes. What if we make a typo in the icon name, like Icon({name: "rihgtArrow"})? Or what if we remove one of the icons from the system, but forget to delete all of the code that uses that icon?

  • We could write runtime checks to catch those cases, like if (!iconNames.includes(props.name)) { throw ... }. But we're writing TypeScript, not JavaScript. We'd rather use the type system to make these mistakes impossible at compile time. A type error during development is much better than a runtime error in production!

  • The code below shows one possible solution. If we call Icon with a correct icon name, it renders the icon. If we call Icon with an incorrect icon name, that's a type error.

  • >
    type IconName = 'rightArrow' | 'billing';

    const icons = {
    rightArrow: 'fake right arrow image',
    billing: 'fake billing image',
    };

    function Icon(props: {name: IconName}) {
    return icons[props.name];
    }
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    Icon({name: 'rightArrow'});
    Result:
    'fake right arrow image'Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    Icon({name: 'billing'});
    Result:
    'fake billing image'Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    Icon({name: 'leftArrow'});
    Result:
    type error: Type '"leftArrow"' is not assignable to type 'IconName'.Pass Icon
  • This solution prevents typos in icon names. It also helps us with maintenance. Suppose that we're doing a redesign, and we want to remove the right arrow icon. When we remove 'rightArrow' from the IconName union, any code that calls Icon({name: 'rightArrow'}) causes a type error. We have to update each of those function calls, either choosing a new icon or removing the Icon(...) call entirely. That's a big improvement over a plain string type, which would let us call Icon({name: 'thisIconDoesNotExist'})!

  • But there's a problem here. We had to list the icon names twice: once in the IconName type, then again in the icons list. When we add new icons, we have to update both of those types. In a system with hundreds of icons, that becomes tedious.

  • Worse, it's possible for the two types to go out of sync over time. We might delete an icon from the IconName union, but forget to delete it from the icons list.

  • We can use the keyof and typeof operators to remove that duplication while simplifying the code. This will take two steps: first we rearrange the types, then we do the simplification.

  • Our first step is to introduce a full Icons type for the icons object. It has one property per icon name.

  • >
    type Icons = {
    rightArrow: string
    billing: string
    };

    const icons: Icons = {
    rightArrow: 'fake right arrow image',
    billing: 'fake billing image',
    };
  • Now we can use keyof Icons to get the union 'rightArrow' | 'billing'. With that code in place, we don't have to write the union out manually. keyof Icons does it for us.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    type IconName = keyof Icons;
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const iconName: IconName = 'rightArrow';
    iconName;
    Result:
    'rightArrow'Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const iconName: IconName = 'leftArrow';
    iconName;
    Result:
    type error: Type '"leftArrow"' is not assignable to type 'keyof Icons'.Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const iconName: IconName = 'billing';
    iconName;
    Result:
    'billing'Pass Icon
  • However, we haven't removed the duplication yet; we've only moved it around. To add a new icon, we have to add it to both the Icons type and the icons array variable.

  • Fortunately, we can use the typeof operator to remove that duplication. First we define our icons object, letting TypeScript infer its type. Then we get its type with typeof icons, which gives us {rightArrow: string, billing: string}. Finally, we use keyof typeof icons to get the type 'rightArrow' | 'billing'.

  • >
    const icons = {
    rightArrow: 'fake right arrow image',
    billing: 'fake billing image',
    };

    type IconName = keyof typeof icons;
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const iconName: IconName = 'rightArrow';
    iconName;
    Result:
    'rightArrow'Pass Icon
  • Now we can use our new IconName type in our original Icon function.

  • >
    const icons = {
    rightArrow: 'fake right arrow image',
    billing: 'fake billing image',
    };

    type IconName = keyof typeof icons;

    function Icon(props: {name: IconName}) {
    return icons[props.name];
    }
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    Icon({name: 'rightArrow'});
    Result:
    'fake right arrow image'Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    Icon({name: 'billing'});
    Result:
    'fake billing image'Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    Icon({name: 'leftArrow'});
    Result:
    type error: Type '"leftArrow"' is not assignable to type '"rightArrow" | "billing"'.Pass Icon
  • In Execute Program's actual Icon React component, we define the IconName type exactly as shown above. A few other modules in the system need to use that type, so they import IconName from the Icon module.

  • Now we can easily add new icons to our icons list. We don't have to update any other code, including the Icon function. The Icon function adapts to changes in the icon list, but it also ensures that we only reference icons that actually exist.

  • >
    const icons = {
    rightArrow: 'fake right arrow image',
    leftArrow: 'fake left arrow image',
    billing: 'fake billing image',
    };

    function Icon(props: {name: keyof typeof icons}) {
    return icons[props.name];
    }
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    Icon({name: 'rightArrow'});
    Result:
    'fake right arrow image'Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    Icon({name: 'leftArrow'});
    Result:
    'fake left arrow image'Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    Icon({name: 'downArrow'});
    Result:
    type error: Type '"downArrow"' is not assignable to type '"rightArrow" | "leftArrow" | "billing"'.Pass Icon