DEV Community

Cover image for Maybe just Nullable?
Pragmatic Maciej
Pragmatic Maciej

Posted on

Maybe just Nullable?

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 
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'))
Enter fullscreen mode Exit fullscreen mode

"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("")
Enter fullscreen mode Exit fullscreen mode

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 be Some, [] will be None

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) };
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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([])
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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)
  ?? []
Enter fullscreen mode Exit fullscreen mode

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 []
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (7)

Collapse
 
iquardt profile image
Iven Marquardt • Edited

A little nitpick: Personally I think that using an Optional<A> is helpful in an untyped setting as well. On the producing side you have a meaningfully named value constructor, which explicitly denotes the created value is optional: Some(x)/None. On the consuming side you need to pattern match, since Optional values are tagged unions. With Optional you always have the Some or the None case. Consequently each use of an Optional value emphasizes its possible absence.

Optional renders the effect of computations that may not yield a result at all explicit. Explicit is almost always better than implicit, right?

Collapse
 
macsikora profile image
Pragmatic Maciej

In languages where Optional is idiomatic I see it as a great staff. As the language itself works greatly with the concept, and far most there is no null concept at all, then I am in. The problem with JS/TS is that there is null, and the whole ecosystem is using it, from core to third party libs. Secondary the language itself evolves into Nullable, in contrary to for example Java which picked Optional.

And yes tagged union has here a benefit that we can make a Functor from it. We cannot make a Functor from Nullable, but we can have kinda substitute which joins map and flatMap in smth similar to Promise.then. So we can make a function chainNullable which will be calling functions until one of them will return null.

About explicit, I don't fully get what you are saying here but with Nullable and TS you are also quite explicit as you see z | null in the types. TS also has optional fields by ? , and this is again idiomatic for Nullable.

For pattern matching, there is nothing like that in JS/TS, but also pattern matching for two cases is not really beneficial. And consumer of Nullable also needs to do the check, and it's not a problem to make an abstraction which also will enforce the else case.

Still the biggest issue I see is - you need to really refuse to use the language and you go into alien concepts from Haskell and others. Don't take me wrong, those concepts are great, but when language has different thing to offer you, it's not so great anymore.

Collapse
 
iquardt profile image
Iven Marquardt • Edited

With explicit I was referring to untpyed Javascript. And pattern matching in JS is usually just an ordinary switch statement, which still makes the cases explicit.

I agree that tagged unions in general and my opinion specifically are not mainstream in JS/TS. Maybe the former will change in TS in the future..

Anyway, good post.

Collapse
 
lucasbernalte profile image
Lucas Bernalte

This is a great article, thanks for sharing your thoughts. When switching to TypeScript this is one of the first things I thought: should I create a Maybe implementation right at the very beginning of the transition? I think I will stick with Nullable as this is already represented in TS, but I am still curious of how things can escalate badly when implementing the Maybe in a TS project.

I also like the idea of a chainNullable in order to compose. The "if not null" check still bothers me a little :)

Collapse
 
macsikora profile image
Pragmatic Maciej

Hey Lucas, tnx for comment. So you can implement Maybe, use it, nothing bad will happen outside of the fact that null will not disappear, and you need to watch out your back, because in any third party lib, the null is still some kind of standard, not surprisingly though. What does it mean is that you will need to protect your project from using any null things, including newest staff like nullish coalescing operator and optional chaining, also you will need to replace standard JS API like Array into some wrapper, as standard Array uses null | undefined. Another subject is communication with server, JSON will send you null fields, those need to be parsed and changed to Maybe, allowing any null to go through your code means that finally you have Nothing | null | undefined as extra-nullish :D.

So to sum it up, you can, but it demands a lot of project rules, and avoiding part of the language syntax and part of the language build interfaces. There is no really going back from that I would say.

Collapse
 
omenlog profile image
Omar E. Lopez • Edited

Hi , in your snippet about getUser I think that should be

const user = await getUser();

Collapse
 
macsikora profile image
Pragmatic Maciej

Asynchronous behavior is not relevant for the whole point. It stays the same, and treat code snippets as totally examples.