Typescript: playing with type operators
Nicolas Erny
Posted on November 3, 2021
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>
);
}
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
}
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];
}
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];
}
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];
}
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
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>
);
}
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>
);
}
This has been helpful for me in my projects. Hopefully it helps you as well.
Posted on November 3, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024
November 23, 2024
November 16, 2024