DEV Community

Cover image for Typescript: playing with type operators
Nicolas Erny
Nicolas Erny

Posted on

Typescript: playing with type operators

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.

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

To make it work, we need to define an object containing all the icon paths and a function to get an icon path.

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
}
Enter fullscreen mode Exit fullscreen mode

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:

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];
}
Enter fullscreen mode Exit fullscreen mode

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:

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];
}
Enter fullscreen mode Exit fullscreen mode

This solution is pretty good. Maybe we could do even better though:

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];
}
Enter fullscreen mode Exit fullscreen mode

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:

keyof typeof iconPaths
Enter fullscreen mode Exit fullscreen mode

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:

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

Oldest comments (0)