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
}
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
}
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
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
}
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
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})
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
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 */
Great! There are no conflicting states. We can easily check now what is the user type by
user.userType === 'Admin'
also it can be abstracted in the function
const isAdmin = (user: User) => user.userType === 'Admin'
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
you have:
u.userType === 'Normal'
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"
Dear Reader, next time don't choose Bool as default. Maybe there is a need for a custom type :)