Home

Mapping Dynamic Object Keys in TypeScript

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",
};

Removing null or undefined Properties on Optional Types

Depending 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!

When Map Properties are not Just Strings

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" },
};

Let's Connect

Keep Reading

Use Dynamic Property Maps over Switch Case Statements

An everyday JavaScript pattern to avoid clunky switch-case statements and unnecessary if conditionals.

May 19, 2022

Converting FormData to JSON in JavaScript

FormData is a useful interface for collecting data from a form. But it can be tricky to convert to JSON.

Apr 28, 2021

Introducing Component Adapters into a Gatsby Project

Component adapters are a great way to separate logic from presentation in component-driven development projects. Here's how I've implemented the approach in Gatsby.

Jul 17, 2020