· Jake Worth

Type-safe string state instead of booleans


When it comes to controlling frontend presentation, developers often rely on booleans. However, this approach has serious flaws.

Here’s something a bit better: type-safe strings. Frontend UI state often starts as a boolean (React example):

component.tsx
import {useState} from 'react';
const [modalVisible, setModalVisible] = useState(false);

When modalVisible is true, the modal is visible. We can then show or hide the modal with its setter:

component.tsx
const showModal = () => {
setModalVisible(true);
// ...user interacts with modal...
setModalVisible(false);
};
return modalVisible ? <Modal /> : null;

It’s time for an additional feature request! Our stakeholder now wants three modals, one to create a thing, one to update that thing, and one to delete that thing.

What should we do? The conventional answer is to add more state.

component.tsx
const [createModalVisible, setCreateModalVisible] = useState(false);
const [updateModalVisible, setUpdateModalVisible] = useState(false);
const [deleteModalVisible, setDeleteModalVisible] = useState(false);

Additional feature request incoming! We’d like to creating, updating, and deleting three different kinds of things on our page.

component.tsx
const [createMeetupModalVisible, setCreateMeetupModalVisible] = useState(false);
const [updateMeetupModalVisible, setUpdateMeetupModalVisible] = useState(false);
const [deleteMeetupModalVisible, setDeleteMeetupModalVisible] = useState(false);
const [createOrganizerModalVisible, setCreateOrganizerModalVisible] =
useState(false);
const [updateOrganizerModalVisible, setUpdateOrganizerModalVisible] =
useState(false);
const [deleteOrganizerModalVisible, setDeleteOrganizerModalVisible] =
useState(false);
const [createTopicModalVisible, setCreateTopicModalVisible] = useState(false);
const [updateTopicModalVisible, setUpdateTopicModalVisible] = useState(false);
const [deleteTopicModalVisible, setDeleteTopicModalVisible] = useState(false);

We’re starting to hate this code. Why? I think it’s a pretty fundamental problem: the use of booleans.

Booleans are a blunt instrument. They have just two states. But many stateful UI interactions need more than two states.

Consider our example above. What are all these state hooks trying to answer? It’s not: “Is this modal visible, and this one, or this one, and this one…?” Rather, it’s “Which modal is visible?”

These modals are exclusive— only one should be visible at a time. And so, this isn’t a case of true or false, it’s a case of which. A boolean is the wrong tool for this problem. There’s a great post linked at the bottom by Matt Pocock that expands on this argument.

So, what’s the solution? Here’s a proposal:

component.tsx
const [modalVisible, setModalVisible] = useState();

To show a modal for creating a Meetup event, we set a string value in state.

component.tsx
const showNewMeetupModal = () => {
setModalVisible('new-meetup');
// ...user interacts with modal...
setModalVisible(undefined);
};
const newMeetupModalVisible = modalVisible === 'new-meetup';
return newMeetupModalVisible ? <NewMeetupModal /> : null;

With this implementation, modalVisible can hold infinite possible single modals, or no modal, with a string or undefined (or an empty string if you prefer). The return logic can be abstracted to function containing a tidy switch statement.

But wait; isn’t relying on perfect strings brittle? One extra dash in "new-meetup" becomes "new--meetup", and no modal appears. Enter type safety! We can type the modal with a TypeScript union, limiting what is allowed.

component.tsx
type Modal = 'new-meetup' | 'edit-meetup';
const [modalVisible, setModalVisible] = useState<Modal | undefined>();
setModal('new-meetup'); // Ok ✅
setModal('new--meetup'); // Type error ❌