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
keyofandtypeofin 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
Iconto 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
Iconis 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.Iconis 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
Iconwith a correct icon name, it renders the icon. If we callIconwith 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'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
Icon({name: 'billing'});Result:
'fake billing image'
- 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'.
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 theIconNameunion, any code that callsIcon({name: 'rightArrow'})causes a type error. We have to update each of those function calls, either choosing a new icon or removing theIcon(...)call entirely. That's a big improvement over a plainstringtype, which would let us callIcon({name: 'thisIconDoesNotExist'})!But there's a problem here. We had to list the icon names twice: once in the
IconNametype, then again in theiconslist. 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
IconNameunion, but forget to delete it from theiconslist.We can use the
keyofandtypeofoperators 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
Iconstype for the icons object. It has one property per icon name.>
type Icons = {rightArrow: stringbilling: string};const icons: Icons = {rightArrow: 'fake right arrow image',billing: 'fake billing image',};Now we can use
keyof Iconsto get the union'rightArrow' | 'billing'. With that code in place, we don't have to write the union out manually.keyof Iconsdoes 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'
- 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'.
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const iconName: IconName = 'billing';iconName;Result:
'billing'
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
Iconstype and theiconsarray variable.Fortunately, we can use the
typeofoperator to remove that duplication. First we define ouriconsobject, letting TypeScript infer its type. Then we get its type withtypeof icons, which gives us{rightArrow: string, billing: string}. Finally, we usekeyof typeof iconsto 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'
Now we can use our new
IconNametype in our originalIconfunction.>
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'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
Icon({name: 'billing'});Result:
'fake billing image'
- 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"'.
In Execute Program's actual
IconReact component, we define theIconNametype exactly as shown above. A few other modules in the system need to use that type, so they importIconNamefrom theIconmodule.Now we can easily add new icons to our
iconslist. We don't have to update any other code, including theIconfunction. TheIconfunction 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'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
Icon({name: 'leftArrow'});Result:
'fake left arrow image'
- 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"'.