Typescript: playing with type operators
Tags: typescript, react
Published: 2021-11-03 16:29:53

Recently, I discovered more advanced techniques in Typescript. It helps me to rely more on type inference and define fewer types.

Let's begin our journey with some React code to display SVG icons.
```js
function App() {
  return (
    <div className="App">
      {["info", "warning", "error"].map((name) => (
        <svg
          key={name}
          fill="none"
          stroke="currentColor"
          viewBox="0 0 24 24"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth="2"
            d={getIconPath(name)}
          ></path>
        </svg>
      ))}      
    </div>
  );
}
```

To make it work, we need to define an object containing all the icon paths and a function to get an icon path.
```js
const iconPaths = {
  info: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
  warning:
    "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z",
  error: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
};

function getIconPath(name: string) {
  return iconPaths[name]; // TS error
}
```

As we define the icon name as a string, we have two issues:
* We get a Typescript error: 

> _Element implicitly has an 'any' type because expression of type 'string' can't be used to index type_

* We have to check if the icon name exists.

It leads to the following code:
```js
const iconPaths = {
  info: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
  warning:
    "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z",
  error: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
} as { [name: string]: string };

function getIconPath(name: string) {
  if (!iconPaths.hasOwnProperty(name)) {
    throw new Error(`Unknown icon name: ${name}`);
  }
  return iconPaths[name];
}
```
It works well. But honestly, we can do better. Let's see if we can add some constraints on the icon name. It could be interesting to check if it exists at compile-time instead of run-time.

The first idea is to define a new type that describes the valid icon names. Here's what that same code would be like:
```js
type IconName = "info" | "warning" | "error";

const iconPaths = {
  info: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
  warning:
    "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z",
  error: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
};

function getIconPath(name: IconName) {
  return iconPaths[name];
}
```

This solution is pretty good. Maybe we could do even better though:
```js
const iconPaths = {
  info: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
  warning:
    "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z",
  error: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
};

function getIconPath(name: keyof typeof iconPaths) {
  return iconPaths[name];
}
```

The type operators (*keyof* and *typeof*) are handy. It helps us to infer the icon name type from the iconPaths object. Now, we don't need to define an explicit type (*IconName*).

Let's talk a second about this type declaration:
```js
keyof typeof iconPaths
```
It means that this type is the union of the keys of the iconPaths object ("info" | "warning" | "error").

Finally, let's try to use the *getIconPath* function in our React component:
```js
function App() {
  return (
    <div className="App">
      {["info", "warning", "error"].map((name) => (
        <svg
          key={name}
          fill="none"
          stroke="currentColor"
          viewBox="0 0 24 24"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth="2"
            d={getIconPath(name)}
          ></path>
        </svg>
      ))}      
    </div>
  );
}
```
Not so fast! With that you'll get the following TypeScript compilation error:
>_Argument of type 'string' is not assignable to parameter of type '"info" | "warning" | "error"'._

The reason for this is because the *getIconPath* function only accepts a certain set of values. Therefore, we have to cast the array of strings to an array of literals.

```js
function App() {
  return (
    <div className="App">
      {(["info", "warning", "error"] as const).map((name) => (
        <svg
          key={name}
          fill="none"
          stroke="currentColor"
          viewBox="0 0 24 24"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth="2"
            d={getIconPath(name)}
          ></path>
        </svg>
      ))}      
    </div>
  );
}
```

This has been helpful for me in my projects. Hopefully it helps you as well.