2025-07-01
Booleans are a fundamental data type in nearly every modern programming language. So why shouldn’t you have them in your language? I’m going to explain the interesting position that booleans occupy in type systems, why they can be so bad, and what we can do about them.
A second null
Booleans are an incredibly common type in modern programming, usually implemented as an always-imported primitive with the name bool or boolean. Booleans occupy an interesting position in type systems: they are a placeholder type for something that needs two values.
To be specific, a boolean is a type with cardinality 2 that supports various boolean operations that operate between elements of the type. It’s generally used when the programmer needs something that can take on two values, which is why its inhabitants are given the abstract names true and false.
Let’s uncover a pattern by looking at the built-in types in a few common languages. I’ll be ignoring composite types, which means I’ll be ignoring types like tuples, arrays, and structs, because they’re not fundamental types themselves, and are instead ways to create types out of other types.
Java has the numerical types byte, short, int, long, float, and double; the textual type char; also null, which isn’t technically a type but occupies a space in the type system and is an inhabitant of all class types (until Valhalla!), so I’m adding it here; and boolean. It’s apparent that outside of number types and textual types, there are the outliers null and boolean. Let’s take a look at some other languages.
Python (source) has number types int, float, and complex; the textual str; singletons like the null object (None), ..., and NotImplemented; and, of course, bool. Python seems to have the same few categories. Let’s try another.
Rust (source) has number types u8, i64, u128, usize, f32, f64, and many more; text-based types like char and str; and bool. This is the first language so far without null or some equivalent.
JavaScript (source) has number types number and bigint; the textual string; the weird symbol; the empty/undefined/nonexistent types null and undefined; and, finally, boolean.
The trend that appears here is that programming languages tend to have number types, text types, null, and booleans. This is due to some cherry-picking, since functional languages such as Haskell tend to not have null (hence Rust not having null, due to heavy OCaml influence), but it is clearly a reality in a large subsection of programming languages today.
Null has been called a billion-dollar mistake; this is due to two main factors: the null unit value being automatically added to all pointer types, and null representing no specific value at all. Booleans are like a second null, but without the more severe issue of being automatically added to other types. The null type is a placeholder for a type with a single value, and booleans are a placeholder for a type with two values. Booleans are like a second null without its most severe downside.
If null instead represented a specific value in the type (maybe zeroed memory) but was not an extra member in the type, it would still be a problem, albeit not a billion-dollar one; it still has the issue of being less useful due to not representing a specific value and yet still being ubiquitous. Why is this so? Well, its abstractness hurts its use significantly. null is such a mistake because it adds an extra inhabitant to (usually) heap-allocated types that has no meaning, and as such provides the ability to communicate senseless information, often resulting in a null pointer error or a segmentation fault. Booleans are the same; the type shares the quality of not having any meaning in itself, albeit being significantly better due to not being added automatically to types, but it is nonetheless much worse at conveying meaning than other types.
Meaningless booleans
As an example of how the abstractness of booleans hurts code comprehensibility, let’s take the function signature fn evaluate(expr: Expr, deep: bool) -> Expr, written in no particular language.
In the second parameter, the name deep is doing all of the heavy lifting here; bool conveys no meaning outside of “this type has two inhabitants,” a heavily contextual idea. It’s not doing a very good job at communicating the meaning, which becomes apparent when looking at the call site of this function: evaluate(expr, true). When reading this call, what does true mean? There’s really no way to figure this out unless you hover over the function—alternatively, some IDEs add parameter name hints to remedy this issue.
There’s also the fact that, within the evaluate function, all normal boolean operators are supported on the deep parameter. Does deep OR other_boolean have any actual meaning? I don’t think so. So why is this allowed?
Improving readability
Is this really a problem, though? evaluate(expr, true) could be more clear, yes, but is there a good way to address this problem? Adding aliases to boolean variants, for example, would hardly improve the readability of the function signature.
The solution to this is dedicated enum types for each use case. This most likely requires proper language support. You don’t quite need types as values, but you need a pretty flexible type system.
Let’s go back to the evaluate example. How can this be more readable? Well, imagine if we changed the signature to fn evaluate(expr: Expr, depth: shallow | deep) -> Expr, where | represents the union of the types on its left and right side; you can provide one, but not the other. You can really implement this however you want, like how languages like TypeScript might prefer "shallow" | "deep", where variants are represented with string literals.
With this change, deep is an inhabitant of the type instead of the name of the parameter, deep explicitly exists in contrast to shallow, and this contrast is given the name depth. This is much clearer, and represents the contrasting ideas much better than “deep” and “not deep”.
This also scales better: if you’re using booleans because they’re a type that happens to have two elements, not because you really need a true and false, it’s more annoying to add potential third variants of the type in the future because you’re using booleans for their convenience, not for their accuracy. Adding a third variant might require refactoring the type entirely, but if represented with enums/unions you could just add another case, like in shallow | until_ptr | deep.
How does this improved version look at the call site? evaluate(expr, .deep) in Zig syntax, or evaluate(expr, "deep") in TypeScript. This looks a lot cleaner, and I’m much happier with how it turns out. Inside the function call, too, operations like depth OR false will now emit a compile error, as they no longer make any sense—something which I think is the correct behaviour.
This isn’t more expensive storage-wise than booleans, since there are exactly as many variants of the depth parameter as there are booleans. This means there isn’t some extra cost to them, provided that the compiler is not stupid.
Booleans as numbers
I’ve been dismissing the idea of needing boolean operators this entire time, but they’re sometimes necessary. Boolean operators like or can be meaningful in certain contexts, so what we are we supposed to use for this instead of booleans?
There’s a simple solution: merge them into the family of numerical types. We’ve already solved the case where booleans are used when the programmer just needs two arbitrarily named variants, but when they’re needed as numbers (like when you need or or and), you can just use number types.
This is also another feature that is likely done best by improved language support; if the language supports integers with a single bit (or integers with any number of bits, or even more generally integers with any number of variants), a boolean is simply a u1: an unsigned one bit integer, which can be 0 or 1.
If you really want booleans…
If you really want booleans, so you need the named true/false variants while also needing boolean operations, you can always fallback to a std.bool implementation in the standard library that implements all of these by itself, without any specific language support. Since bool = true | false, this should not be difficult.
If statements
if statements depend on booleans existing in the language. They handle a special case of switch statements over an enum or union. Without booleans, they’ll need another way to know which variant to select. I don’t think this is a big deal; there are several ways to make if statements work after removing booleans. Primarily:
- A way to check variants, such as if (value) is .deep { code }.
- A way to denote a default variant in the the union, so that if (value) is equivalent to if (value) is .variant.
- Supporting if statements for u1.
- Having if statements work solely over unions of two types and somehow determining a truthy vs falsy one.
You can implement multiple of these at once, and they also overlap (e.g. the third option is a specific instance of the second option), so altogether I don’t think that this is an issue.
Why talk to the designer?
Why am I talking specifically to programming language designers, instead of users of the language? Well, at its core it’s a programming language design issue, but it also is something best done in the language itself. If a language doesn’t have a flexible type system to the point where you can’t define shallow | deep inline in the parameter type, it might not be worth it to avoid using booleans if the language doesn’t want you to do that. Ubiquitous usage of booleans in the standard library or other existing tooling is also hard to avoid.
If a language doesn’t have support for 2-inhabitant integer types, you risk introducing another problem by using numbers in place of booleans: is 3 truthy? Is -5 truthy? Who knows? If you always use a full byte to represent a boolean, it’s much easier on the programmer side to use one of the 254 other states with potentially undefined meaning.
Some languages do have good support for this, without entirely removing booleans! Zig, for example, supports both union types like enum { shallow, deep } and integers of any bit width, like u1 (and, eventually, hopefully any cardinality, too!). In these cases, it’s best to generally avoid using integers, but sometimes it’s necessary to interface with them.
The final reason is simply that I’m a programming language designer myself! I’m working on a language that implements features like this, so this is a real thing I’ve run into and had to solve. This also helps back up what I’m saying, since I’m not just spitting out recommendations without being forced (via the development process) to understand their consequences.
Nicole’s post
I wrote this post because I was inspired by Nicole’s post on recommending to the user what a programmer should use instead of booleans. I had already discussed this in the Zig Community Discord a few times, and the idea of omitting booleans from the language has been addressed in the notes for my (hopefully) upcoming language for a few weeks now, but reading her post on this inspired me.
To actually address the content of the post: I don’t fully agree with the idea, and it’s not exactly what I’m arguing since Nicole chooses to focus on information that you’re potentially omitting when you store just a boolean, but it’s definitely a pretty similar concept to what I argue here, and was definitely a great read.
My recommendation
Since this is a change best implemented by the language designer, my final recommendation is to replace booleans with enums/unions/sum types for cases where they’re used as two-value types, and with numbers if they’re used numerically. Of course, this should also come with proper language support, including a std.bool implementation if you really think you need them.
By avoiding booleans, languages can be made more minimal and less complicated to initially understand, while also improving programs written in that language by making them more readable and comprehensible, as it forces the programmer to assign more specific meaning to each of the two variants they’re dealing with.
Thanks for reading.
Found my content on a site or in a video? Any questions or corrections? Please let me know on Discord at @GoldenStack
Website design stolen from Cody @ codyq.dev
This site does not contain any JavaScript.
.png)


