Presence is Boolean
- 4 minutes read - 652 wordsWhen possible, I prefer to use the presence of data to represent a boolean, rather than a boolean itself.
Example: Error State
Here’s a bug vector I see a lot in frontend code. Imagine we’ve made a network request that returns an error. Our code sets a boolean errorState
, meant to convey “we are in an error state”, to true
. Then we display an errorMessage
in that condition:
if(isErrorState) {
displayMessage(errorMessage)
}
The problem here is that we now have two variables tied to our error condition: isErrorState
and errorMessage
. And when that happens, it’s easy to put the app into one of these illogical states:
- We’re in an error state… but there’s no error message. What are we going to show?
- We’re not in an error state… but there’s an error message. Should we show it?
Logically, they should both either be truthy or falsy; any disagreement is a problem. And yet, there’s nothing stopping that from happening.
Rather than creating both a boolean and a string, a pattern I prefer is to let the string’s presence represent the boolean.
if(errorMessage) {
display(errorMessage)
}
For further exploration of this type of issue, check out Matt Pocock’s State Management: How to tell a bad boolean from a good boolean.
Example: Data Presentation
Another example is some conditional view logic that looks like this:
// functions
const canDisplayName = () => data.firstName && data.lastName
const displayName = () => `${data.firstName} ${data.lastName}`
// template
canDisplayName() && <p>{displayName()}</p>
The first function, canDisplayName
, is a little indirect. Both it and the function that it guards know the same thing, the two names.
Here’s a version where the presence of data becomes the boolean.
// function
const displayName = () => {
if(data.firstName && data.lastName)
return `${data.firstName} ${data.lastName}`
}
}
// template
displayName() && <p>{displayName()}</p>
The presence of firstName
and lastName
returns a string like "Jake Worth"
or undefined
. Hence we get truthiness and falsiness and the display data in one
function.
This becomes advantageous when displayName
needs to change. Instead of two or more functions all reading the same data, there’s just one.
Example: Application Configuration
Application configuration is fertile ground for this mistake. Consider a boolean in the environment called SENTRY_ENABLED
and a string SENTRY_URL
. This design makes it easy to put the app in a state in which Sentry is enabled, but has no URL for Sentry to talk to. A better choice is to let the presence of SENTRY_URL
tell us whether Sentry is enabled. You can even codify this in a helper method.
def sentry_enabled?
ENV["SENTRY_URL"].present?
end
Example: Database Booleans
One more example on the backend: boolean columns in a database.
It’s very common to see a database table with a boolean column like deleted
to represent a soft-delete. When true, we soft-deleted the item, and when false, we haven’t. Every time you are about to write one of these, stop and think: could this be a nullable timestamp?
Nullable timestamps such as deleted_at
can act just like a boolean; if present, they are true, and if null, they are false. The benefit of this approach is that it helps with debugging– we know when the soft-delete was initiated, rather than only that it was at some point in time.
In this example, the presence of the timestamp represents a boolean value, and we get the benefit of more debugging information and discoverability as a byproduct. And, we can query using the timestamp to see just records soft-deleted in a certain time range.
Is this overkill? If you never do it, perhaps. It’s become my sensible default, something I just do. I want a database that has a lot to tell me when something breaks.
Conclusion
I’ll conclude with a quote from the Matt Pocock blog post I mentioned earlier. It sums up a winning approach to boolean management:
Bad booleans represent state. Good booleans are derived from state. – Matt Pocock