Dynamic property maps are a super powerful paradigm in JavaScript, but they can be tricky to type correctly with TypeScript.
A pattern I use on an almost daily basis is dynamic property maps. It helps me avoid unnecessary if/else and switch/case statements.
But it’s a tricky scenario to get right with TypeScript. Consider the example from the dynamic property map post:
const buttonClassMap = {
dark: "bg-black text-white",
light: "bg-gray text-black",
};
const theme = "light";
buttonClassMap[Object.keys(buttonClassMap).includes(theme) ? theme : "dark"];
The beauty of TypeScript is that if theme
is defined elsewhere in the code, we can ensure it’s the right type and not need to do this checking.
The problem is that it’s not as straightforward as it seems it should be. Let’s add a Button
type with a theme
property, and then assign our theme
variable to that type. Something like this:
type Button = {
theme: "dark" | "light";
};
const buttonClassMap = {
dark: "bg-black text-white",
light: "bg-gray text-black",
};
const theme: Button["theme"] = "light";
buttonClassMap[theme ?? "dark"];
We’ve simplified the last line, and it seems like we’re type-safe. But we’re not fully in the clear. I can add new properties to buttonClassMap
without error:
const buttonClassMap = {
dark: "bg-black text-white",
light: "bg-gray text-black",
// We want this to throw a type error
other: "...",
};
That means we have to type the buttonClassMap
. We can do that by using a mapped type:
const buttonClassMap: { [K in Button["theme"]]: string } = {
dark: "bg-black text-white",
light: "bg-gray text-black",
// Now this throws a type error
other: "We do not want this to be allowed",
};
null
or undefined
Properties on Optional TypesDepending on your compiler options, you may see a type error if you make theme
an optional type (by appending a ?
to the key).
The way to solve this is to ensure that K
can’t be undefined
. We can fix that using the Exclude utility type.
export type Button = {
theme?: "dark" | "light";
};
const buttonClassMap: {
[K in Exclude<Button["theme"], null | undefined>]: string;
} = {
dark: "bg-black text-white",
light: "bg-gray text-black",
};
const theme: Button["theme"] = "light";
buttonClassMap[theme ?? "dark"];
And now you should be free of TypeScript errors!
If your map properties are objects or arrays instead of just strings, you can type them just like you would any other type. For example, suppose we wanted the background and text classes to be properties within an object, we could do something like this:
const buttonClassMap: {
[K in Exclude<Button["theme"], null | undefined>]: {
bg: string;
text: string;
};
} = {
dark: { bg: "black", text: "white" },
light: { bg: "gray", text: "black" },
};