Maybe just Nullable?
Pragmatic Maciej
Posted on May 6, 2020
Problem of optional value, it's not a trivial one, and for sure not a young one. You probably have red the famous quote about null
I call it my billion-dollar mistake. It was the invention of the null reference in 1965 Tony Hoare
Fortunately, newer languages can deal with absent values better, and the older languages get updated by this new approaches. We for sure live in better times in terms of null problem solving. One of these approaches, and probably the famous one is Optional/Maybe. But should we use this concept in every language, and should we use in language like JavaScript?
I invite you into deep dive into handling absence in JavaScript, TypeScript and other languages. Buckle up, and let's go ðĒ!
About Optional
Maybe/Optional is a famous data structure, the concept is about wrapping the value inside a container ðĶ, and the container can have the value inside, or not. In other words we work not directly with the structure, but we work with opaque data covering it inside. Container gives us a specific interface to work with such value. I will slowly reveal parts of the Maybe interface.
At the type level Maybe is represented as:
type Maybe<T> = Some<T> | None
// names are examples, it can be also Just, Nothing or any other meaningful name
I will not get into implementation details of Maybe, but implementations can be many, it can be class (or typeclass ð), it can be a simple object with functions working with it, we can even make Maybe from Array, where no value is represented by an empty array []. There are few rules our creation needs to hold though, but I will not include them into the article, lets focus on the practical aspects.
Note Pay attention that in the whole article I will be using these two names - Maybe, Optional interchangeably for the same thing.
The promise of the better null
Typical introduction to Optional describes it as something far more better than null checking, but examples are at least questionable. Take a look at this prime example of using Maybe.
function divide(a, b) {
if (b === 0) {
return None();
}
return Some(a / b);
}
const optionalValue = divide(1,2) // result is or None or Some
if (optionalValue.isSome()) {
// do smth
}
I hope you agree with me that it does not look far better than the null check, or even more it looks the same! But don't take that as disbelieving in the whole Maybe concept, its more a showcase of how we can make wrong arguments, and this argument looks wrong ð.
JavaScript idiomatic absence representation
JS has more than one representation of absence, it has two - null
and undefined
. It's not a good sign, as there is no way to check that directly in the single equality check, we need to check two values or take into consideration that our condition will work also for Falsy, Truthy values.
We know that even such simple code in JS is already buggy:
if (x) {
// yes x is there
} else {
// no x is no there
}
The fact that we are inside if
block doesn't mean x
is true
or the value is there. It will get into a positive path whenever x
is Truthy, so every value outside: false, null, undefined, 0, empty string or NaN
. That's not great definitely, and don't point at me "You don't know JS" books please ð. But for ages there was a simple solution for this issue.
// function which unifies null and undefined (name is example)
function isAbsent(x) {
return x === null || x === undefined
}
// for better readability lets create the opposite
function isPresent(x) {
return !isAbsent(x)
}
// now in action
if (isPresent(x)) {
// yes x is there
} else {
// no x is not there
}
Simple don't you think? There are two great things in the isAbsent
function, it removes Falsy values problem, and it joins undefined
and null
as one thing.
Let's take the divide example and solve it with idiomatic JS null value.
function divide(a, b) {
if (b === 0) {
return null;
}
return a / b;
}
const value = divide(1,2) // result is or null or number
if (isPresent(value)) {
// do smth
}
As we can see, there is no significant difference between this and the previous code. But remember, Optional needs implementation, as it is an additional abstraction, in contrast null
was and is in the language.
Say Hi to Nullable
So, what is the name of this idiomatic behavior, commonly the name for a value or null is Nullable. Nullable in the type system can be written as:
type Nullable<T> = T | null
However as we have previously mentioned, we have two representations, then the proper definition would be:
type Nullable<T> = T | (null | undefined) // brackets only for readability
Now, you can think, yhym but it looks almost the same as Optional. No, its different, lets see both shoulder by shoulder
type Nullable<T> = T | (null | undefined)
type Optional<T> = Some<T> | Nothing
The difference is that Optional is a container ðĶ, where Nullable is flat/plain union. This fact makes it impossible for Nullable to contain inside another Nullable, where Optional has no issue to have inside another Optional. To put it another way, Nullable cannot be nested.
In conclusion we have two solutions for the same problem. What are the differences, how use one, how use another? In next chapters we will compare using these constructs in JavaScript/TypeScript.
Note pay attention that for different languages this comparison will look different. Not take points I make as general, they are more in the context of specific languages.
Note in JS terms there is another name - nullish.It refers to values -
null
|undefined
. Therefore we can say Nullable is value or nullish.
Using optional value
Because Optional is a container, we cannot just use the value directly. We need to take the value out. Very popular name for such Optional functionality is withDefault
or unwrap
. For Nullable there is no additional abstraction, we can use language operators directly. Lets see that in the code.
// Optional version
return value.withDefault(0) + 1;
// Nullable version
return (value ?? 0) + 1
The benefit of Optional (debatable) will be here readability of this code, also if value would not be Optional, this line would fire the exception, what is at least better than implicit conversions and pretending everything is ok ðĪ·ââïļ.
The second approach with Nullable uses quite recent ??
operator which unifies undefined
and null
(remember what we did with isAbsent function, do you see similar approach here? ð), so if the left side is one of those values(null or undefined) it will fallback to the right operand. Its important to say that ??
removes Falsy values problems existing with previous approach with ||
. The clear benefit is again the fact that it is an idiomatic language approach, no additional abstraction included.
Methods and fields of value which can be absent
The famous error "undefined is not a function" happens when we have undefined, but we want to use it as a function. How can we deal with this problem by our two approaches?
// Nullable
userNullable?.setStatus('active')
// Optional
userOptional.map(user => user.setStatus('active'))
"Map" function allows us to run the code only if the user is there, for None
it will not call it, so we are totally safe.
We see here the same difference as before, one is idiomatic by ?.
optional chaining operator (it unifies null and undefined as single absence value ð), second is additional abstraction in form of "map" function. You can recall map
from Array, and yes this is exactly the same concept.
Note better name for
?.
would be nullish chaining operator, as current name creates a confusion with Optional, but it has nothing to it.
Access to nested fields
Consider a not so strange situation with nested optional object. How to deal with this problem?
// Nullable
user?.comments?.[0]?.content ?? ""
// Optional
Optional.fromNullable(user)
.map(user => user.comments)
.flatMap(comments => Optional.fromNullable(comments[0]))
.map(comment -> comment.content).withDefault("")
Quite a difference don't you think? For sure there is a lot of ?
with Nullable, as these are null chaining operators and nullish coalescing operator. But on the other hand the Optional part looks much more complicated. As you can see, we not only used map
but also flatMap
. The second allows us to chain functions which will return Optional, if we would do it in map
the end result would be Optional inside Optional, and naturally we need to make it flat.
Did you notice that Array also has flatMap method? And yes it has the same purpose and type definition as our Optional.flatMap. So we already see at least three similarities:
- both are containers
- both have map
- both have flatMap
There needs to be some hidden treasure ð in here.
Note as said before Optional can be made from Array.
[value]
will beSome
,[]
will beNone
JS has null, JSON also has it
I have said null value is idiomatic to JS, but also it is idiomatic to most popular data transfer format - JSON, no surprise as it is JavaScript Object Notation. We can have nulls in the server response/request, but we cannot have Optional values, there is no such thing in JSON.
How to deal then with nulls from the API. There is a popular approach called "fromNullable". Consider getting data from the server and using Optional.
const user = async getUser()
const userDecoded = {...user, secondName: Optional.fromNullable(user.secondName) };
What we did here is decoding secondName
field value from Nullable into Optional. What about Nullable approach? Its idiomatic so you don't need to do nothing and you have it, it's again 0 cost for Nullable.
Note maybe this example didn't make you worried, but the difference is huge. With Optional there needs to be always decoding of every server response. Bye, bye using data directly.
Note In contrast to the previous note, decoding/validating input is a good practice.
The JS ecosystem and build functionalities
Most of the code you will encounter will work with nulls, you can encounter libraries working with Optional, but as I said before there is an infinite ð amount of possible implementation of this pattern. So be sure, if you made your own Optional, you need to parse every null in the code.
For the example we will use Array.prototype.find
. In order to work with it, and with Optional, we need to understand that it returns undefined
. It means we need to use our friend fromNullable
again. In order to not repeat ourselfs, let's wrap it into another function.
function findInArr(arr, predicate) {
return Optional.fromNullable(arr.find(predicate));
}
And we need to use this wrapper in our code base instead of Array.find
, always. Yes always!
But what if I have an array inside an array and want to do some filtering?
// Nullable version
posts
.find(post => post.id === id)
?.comments
.filter(comment => comment.active)
// Optional version
findInArr(posts, post => post.id === id)
.map(post => post.comments)
.map(comments => comments.filter(comment => comment.active))
As you can see again map
has saved as, but take a look that we've nested inside map another higher order function call, where in Nullable composition remains flat.
Optional likes functions, Nullable doesn't
Functional programming, yes that is the familiar land for the Optional concept, therefore functions are the thing what makes Optional happy. Optional allows for using functions which don't care if something can be absent, the whole problem covers Optional, and all functions around are free from checking that. Maybe it looks like not a big deal, but believe me its huge code reuse!
// some functions which are not aware about optionality
const withUserName = name => user => user.name === name ? Some(user) : None()
const userComments = user => user.comments
const activeComments = comments => comments.filter(c => c.active)
// using
const userComments = optionalUser
.flatMap(withUserName("John"))
.map(userComments)
.map(activeComments)
.withDefault([])
As you can see all declared functions have no wisdom about optionality of the user. All these functions work with values as always there. Optional takes out the whole problem of absence from all functions in the codebase.
Could we be using these functions with Nullable also? No, Nullable has no way to call these functions without temporary variables. Lets see the code:
// we need to redefine withUserName in smth like that
const isUserWithName = name => user => user.name === name
if (isAbsent(user) || !isUserWithName("John", user)) {
return null;
}
activeComments(userComments(user));
As you can see, there is no idiomatic way to call such functions without repeating the condition. Nullable is not a functional programming concept, the same as ?.
and ??
operators. When you look at Optional with functions you see the flow, you see the pipe of data going top->down. When you look at Nullable version, it's much worse, there is no one clear data flow, part of function calls are combined by ||
part by just function composition f(g(x)
. Not a great staff.
Nullable is not Optional, therefore don't use it as Optional
When we try to use Nullable as Optional, then the code can look so bad as I showed in the previous chapter. But when we switch our mind, we can also use some functions in the Nullable chain. Now rewritten example, but with Nullable way of thinking
const withUserName = (name,user) => user?.name === name ? user : null
withUserName("John",user)
?.comments
.filter(c => c.active)
?? []
As operations are trivial, I have only taken out the withUserName
function. With longer chains there is possibility of reuse of more parts of the code into functions. I could be reusing for example filter predicate, but it's trivial and IMHO should be an arrow function. I have written more about that in the article - Not every function needs a name.
But can I use both? Why not?
As you can see parsing/decoding every null value into Optional can be a burden. We don't want this burden, so lets maybe use Optional in some places, and Nullable in others? It's a fatal idea, it means we extend already existing two values representing absence by third - "None". And the whole codebase will be a mystery when we have null, when we have Optional, and when we have just safe values to use. If you want to use Optional you need to force using it everywhere.
Note with TS it would be known where is Nullable, where is Optional, but still having them both its nothing good. We get additional decision process.
Are we safer in JS by using Optional?
No, I am sad to say that, in JS nothing will give you safety. In the same way you can use null as a function, you can also use Optional as a function, or as a string or whatever you want ðĪŠ.
We are not even a bit safer with Optional, we had issues with null values, we will have the same issues with Optional values, as we still don't know when it is Optional, and when it is plain value. Why is that? Because we work with dynamically typed language, and safety is not a design goal of such. If you don't know what can be null, you will still have defensive checks, but instead of ifs you will have maps and flatMaps.
Static types, does they change the picture
Yes and no.
Yes. With TypeScript we have knowledge what can be absent, therefore both Nullable and Optional are visible, and optional value cannot be just used as a present one. Every try to use such value in not a safe way, will make compilator mad ð .
No. Other points from JavaScript hold also in TypeScript. We have a lot of burden with using Optional, there is no simpler way here.
Both solutions, Nullable and Optional, in a static types land fix the Null issue. With TypeScript we know when value is optional. Because we know when to make if, or .map our code will not overuse nor conditions nor abstraction.
Maybe just Nullable?
So where are we now, what should we use? I have presented many use cases of both things, I hope you see how Nullable is idiomatic and works well with the language, and how Optional is a kinda alien concept. It's sad my FP friends, but JS is not a good land for Optional, Optional lives well in the land of Haskell, Elm, Reason and other functional static typed languages, but in JS/TS its a lot of work to use it.
My personal opinion for plain JS is rather harsh, I would not recommend using Optional, I would recommend Nullable as the language went into that direction with optional chaining and nullish coalescing operator. Even if pipe |>
operator will land in JS most problems with Optional will remain unfortunately.
The TypeScript situation isn't different, I suggest to pick Optional only if we want to go fully into the functional rabbit hole, and you write mostly functions and expressions. You can consider two libraries to start - fp-ts and io-ts.
Note pay attention that fp-ts is almost like additional language on top of TS, this is exactly example of going fully into the rabbit hole
Optional lives happy in other languages
Even in the FE land there are languages where Optional is an idiomatic way of handling absence. Languages like Elm, ReasonML, PureScript are using Optional as a primitive for absence handling. Another benefit is the functional nature of these languages, pipe, compose, currying are just there out of box. Below some Elm code, which covers one of our previous examples:
-- Elm
withUserName name user = if user.name == name then Just user else Nothing
optionalUser
|> Maybe.andThen (withUserName "John")
|> Maybe.map .comments
|> List.filter .active
|> withDefault []
As you can see language has field access ".field" as a function ðē, currying and pipe operator ð, and most importantly Maybe is just a single primitive for covering absence. Every library core, third party library will use exactly Maybe. To put it another way we don't need to fight with the language.
In contrast below small snippet from Kotlin which uses Nullable:
// Kotlin
val b: String? = null // b is nullable string
println(b?.length ?: -1) // -1 if the left operand will be null
Does it look similar to our JS snippets? Surely it does!
Some languages use Nullable some Optional
These concepts are known also in other languages, and some of languages pick Nullable, some Optional. Take a look at below list (its not complete):
- Optional: Swift, Rust, Haskell, Elm, OCaml, Scala
- Nullable: C#, TypeScript, Kotlin
- Wannabe Nullable: JavaSciript, PHP, Python
Excuse me for the last one, if you are a dynamic typed languages fan. But the real problem is that we don't know what can be null, this problem is not addressed in dynamic typed languages.
As we can see, for some languages Optional is idiomatic, for some Nullable. TypeScript and JavaScript are languages where Nullable is idiomatic.
Summary
If you think in a pragmatic way, and you want to use language constructs then use Nullable, if you are functional programmer, and you are aware of the whole effort you need to make then try your luck with Optional, but take into consideration that for now both TS/JS have idiomatic absence value and it is "null | undefined" (nullish). Remember though, going into Optional will force not only you to refuse idiomatic working with the language, but also every team member you work with.
My advice is - use the language, don't fight with it, don't pretend it is a different one.
Thank you!
Posted on May 6, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.