DEV Community

Cover image for Boolean - The Good, The Bad and there is no place for the Ugly
Pragmatic Maciej
Pragmatic Maciej

Posted on • Updated on

Boolean - The Good, The Bad and there is no place for the Ugly

Bool, Boolean, we all know that type. It is a primitive type in every programming language I know. Bool is a type containing two possible values - True and False. That means that Bool is very small set of possibilities. This property of Bool is its strength, if Bool is used when it should be, but it is also the biggest weakness when it comes to using it wrong.

I will try to convince you that you should think twice before representing a state part by Bool.

Lets say we have User, I will write user contract using TypeScript notation. Also code examples in this article will be in TS. Hope you don't mind and it will be readable enough.

type User = {
  id: string
  name: string
}
Enter fullscreen mode Exit fullscreen mode

Ok, easy peasy. Now business is saying that we do have admin among other users, and there is a different functionality for those. Aha, so the simplest way is to create a flag. Below User with this small change

type User = {
  id: string
  name: string
  isAdmin: boolean
}
Enter fullscreen mode Exit fullscreen mode

Nice, so in code now, it is simple to check if user is an admin or not. I will create a function for checking that

const isAdmin = (user:User) => user.isAdmin
Enter fullscreen mode Exit fullscreen mode

Not very sophisticated, but lets continue. Ok now we have our different behavior, lets assume quite a lot of code was done with using our isAdmin flag. After that business comes to us and says - we have also moderator. And moderator is different kind of the user from normal user or from admin user. Crap, what can we do with our isAdmin flag now. Lets try to continue with these booleans then, and create another one

type User = {
  id: string
  name: string
  isAdmin: boolean
  isModerator: boolean
}
Enter fullscreen mode Exit fullscreen mode

Nice, but, not quite. The problem is that the code has introduced hidden dependency between state properties. Where, where? Ye, so dependency is between isAdmin and isModerator, as user cannot be moderator and admin in the same time (that says business). Ok, so taking that into consideration, it looks like there exists conflicting state, and I need to defend the app against that state. The conflicting state is

isAdmin: true, isModerator: true
Enter fullscreen mode Exit fullscreen mode

This just cannot happen, but the type doesn't say it can't. From the type perspective it is totally valid shape. Lets fix this in the code, and create functions which will create our user with different types.

/* ... - is not spread operator but just a placeholder, I just skip rest of the code */
const createNormalUser = (...) => ({.., isAdmin: false, isModerator: false})
const createModeratorUser = (...) => ({.., isAdmin: false, isModerator: true})
const createAdminUser = (...) => ({.., isAdmin: true, isModerator: false})
Enter fullscreen mode Exit fullscreen mode

Ok, we are saved, but only temporary :( . After a longer while, there is new requirement. Fourth type of the user - Manager. Crap, bigger crap then the last time. As for two Booleans amount of combinations was - 2 power 2 = 4, then for three it is 2 power 3, so 8 combinations already. And more conflicting states, for three Bools, there are such conflicting states

isAdmin: true, isModerator: true, isManager: true
isAdmin: false, isModerator: true, isManager: true
isAdmin: true, isModerator: false, isManager: true
Enter fullscreen mode Exit fullscreen mode

So for 8 combinations, 4 are just invalid [(true, true, true), (true, false, true), (false, true, true), (true, true, false)]. In this time you should see where this is going. Next requirement gives us 16 combinations, and so on. This approach just cannot be sustainable in that shape. What should be done instead?

Custom type for the rescue.

Let's remove the limitation of Boolean and properly design the state. The reality is that our User can have different type. So the proper model should be

type User = {
  id: string
  name: string
  userType: UserType 
}
type UserType = 'Admin' | 'Normal' | 'Moderator' | 'Manager' 
/* Yes, UserType can be also represented as Enum type */
Enter fullscreen mode Exit fullscreen mode

Great! There are no conflicting states. We can easily check now what is the user type by

user.userType === 'Admin'
Enter fullscreen mode Exit fullscreen mode

also it can be abstracted in the function

const isAdmin = (user: User) => user.userType === 'Admin'
Enter fullscreen mode Exit fullscreen mode

As you can see, it is also more explicit, in contrary to that check

!u.isAdmin && !u.isModerator && !u.isManager // it means it is normal user
Enter fullscreen mode Exit fullscreen mode

you have:

u.userType === 'Normal'
Enter fullscreen mode Exit fullscreen mode

Sweet 😉

Ok, what we gain by this approach:
✅ it is extendable
✅ it removes conflicting state shapes
✅ it is more explicit
✅ it removes complexity in checking many Bool fields

Let's go to the title Boolean - The Good, The Bad and, and nothing really. Bool can be either The Good or The Bad, only two options are possible, so the definition of the famous western (The Good, The Bad and The Ugly) main characters is not representable as Bool. There is a need for custom type again 😁

type CharacterType = "Good" | "Bad" | "Ugly"
Enter fullscreen mode Exit fullscreen mode

Dear Reader, next time don't choose Bool as default. Maybe there is a need for a custom type :)

Latest comments (11)

Collapse
 
samholmes profile image
Sam Holmes

If the application deals with admins and non-admins, then booleans are just fine. If you're application deals with more than two states for a "user type", then use a enumerable data type. Booleans are just fine when used properly.

Collapse
 
macsikora profile image
Pragmatic Maciej

Hi Sam. Thanks for the comment.
Yes Booleans are fine. But I see in this particular example and in many others even for two possible values I would choose some Union type. The reason is that Bool is not informative enough. So if you have property isAdmin and it is false, then you know that the user is not an admin, but who is the user, still you don't know.

And by custom type (enum, union) it can be easily defined. So I can set a type like (not TS notation) Admin | Manager or Admin | Client and so on. And I can define who is this second part. I know then, not only that somebody is not an admin, but I know who exactly he is.

In other words, such type gives identity to every option existing in our type.

Collapse
 
mrlarson2007 profile image
Michael Larson

Just one thing would like to mention, this is a great example where boolean flag would not be a good idea, but what if we where guaranteed that there only be admins and non admins and nothing else? We have to make trade offs all the time and there may be cases where a boolean flag is just fine.

Collapse
 
macsikora profile image
Pragmatic Maciej

Yes true, we make trade off all the time. I think in this particular example even for two options, type like: Admin | User would be just more readable. But even though you would choose Bool for that, then most important is to know when to turn back. The moment here would be any first situation when you need to create a second Bool, this should be a red light, it indicates that our data is wrongly modeled.

Collapse
 
srobfr profile image
srobfr • Edited

This works well until the business says that a Thing in your model can be updated only by (Managers) and (Normal users that own this Thing).
Then you have to bin all this and implement a roles system with an access control list.
This is the normal life of an evolving project...

Collapse
 
macsikora profile image
Pragmatic Maciej

Users and roles was only naive example. Not like I wanted to go deeper in that domain, it was just for me natural fit to show the problem in clean way.

Collapse
 
srobfr profile image
srobfr

You did it very well :)
Also, boolean function arguments are often considered a code smell, as they are mostly used to implement things that should be done with the Strategy design pattern.

Collapse
 
qm3ster profile image
Mihail Malo

I think you accidentally wrote

type User {
  /* props */
}

instead of

interface User {
  /* props */
}

in most your code examples.

Collapse
 
macsikora profile image
Pragmatic Maciej

Nope, it was intentional, but I did an error in syntax. Thanks! There is = sign missing. Will fix it. Thanks again

Collapse
 
qm3ster profile image
Mihail Malo

That's an option as well.
I tend to use interface whenever possible however, because (at least in the past) there were edge cases where the typechecking is more strict.
Plus, the error messages are also clearer.

Collapse
 
caleb_rudder profile image
Caleb Rudder

This is Great! In my applied algorithms class we had many cases and at the beginning of that semester my code was awful and littered with if statements and boolean values. However, I learned rather quickly (with the help of the prof) the efficiency and cleanliness of custom data types and structs.