What is wrong with optional chaining and how to fix it
Tags: typescript, javascript, webdev, functional
Published: 2019-11-07 23:39:24

**Edit:**
There is nothing wrong with optional chaining, the feature is related to idiomatic absence value in JS, and it is "null | undefined". The operator tries to address issues of previously used `&&`. This article tries to make a point that JS has Nullable, and not Optional. I don't agree anymore with points I made here, but leaving this article untouched. 

Optional chaining, fresh feature released in TypeScript[ 3.7](https://devblogs.microsoft.com/typescript/announcing-typescript-3-7/), as it went lately into stage 3 of ECMAScript standard. This is a great feature but there are some issues with using it. Issues are maybe not fully related with the proposition itself, but more with current state of things, and how JS needs to be compatible backward in order to not break the web.

## The good part

Let's start from what the feature solves. And it solves two issues:

1. Nested conditional checks in nested nullable properties
2. Falsy, Truthy issues during checks mentioned in point 1


### The first

Instead of nested conditions, or many `&&` we use `?.`.

```ts
// the logical and operator way
x && x.y && x.y.z
// the optional chaining way
x?.y?.z
```
Also it is very nice for using methods in objects. Consider:
```ts
x?.y?.z?.filter(filterFunc) // where x, y, z are nullable values and z is an array
```

### The second

Different way of viewing what really means no value. Optional chaining brings a new rule to the table, instead of considering something as [Falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) `null | undefined | empty string | 0 | NaN | false`. Optional chaining simplifies above and removes a lot of errors by saying that values considered as no value are only two - `null | undefined`. 

Examine the code which works badly:

```ts
function getLengthOfStr(x) {
  return x && x.s && x.s.length; 
}
getLengthOfStr({s: ''}) // returns empty string!
```
For empty string `{s: ''}` it should give us 0, but it will return empty string!. Optional chaining fix that nicely:
```ts
function getLengthOfStr(x) {
  return x?.s?.length; 
}
getLengthOfStr({s: ''}) // return correctly 0
```

## The bad part
That is great feature, but is also highly not consistent with the previous behaviors of the language. Consider below code:

```ts
const value = x?.y?.z; // z is a optional number
if (value) {
    return value + 1;
}

// or more concise
if (x?.y?.z) {
    return x.y.z + 1;
}
```

Can you spot the issue?

The issue is in different behavior of new concept with the old one. In situation where `z` equals 0, this code would not add 1, as `if` is working by previous rules, so 0 is considered as *Falsy*. What a crap :(.

The fix is:
```ts
const value = x?.y?.z; // z is a number
if (value !== null && value !== undefined) {
    return value + 1;
}
```
So the thing is that we need to use old, good solution like:
```ts
// simplified typing with use of any
function isNull(x: any) {
  return x === null || x === undefined;
}
const value = x?.y?.z; // z is a number
if (!isNull(value)) {
    return value + 1;
}
```
Better but this shows that the new feature is crippled by it's descendants. Inconsistency of the language is really quite an issue, even bigger now after this change.

> To be clear the change is very positive, but if you imagine code like `a && b ?? c?.d || x` it can be really hard to reason about that.

That is not the end. Lets say I do have a function which I want to call on the property which is a result of the optional chaining. We can do that by previous `&&` operator. Below example

```ts
// func - function which works on NonNullable value
// it can be applied by previous && syntax
x && x.y && x.y.z && func(x.y.z)
```
Can it be done like that in the new one? Nope, it can't :(. We need to use `&&` again.

```ts
 x?.y?.z && func(x.y.z)
```
Unfortunately both versions have the same issue, for `z` being empty string, it does not call `func` function. Another issue is that in the second we join two operations which have totally different rules of behavior. Implicit complexity is arising. 

How then properly call this function on the optional chaining result?
```ts
// lets create another typeguard with proper typying
function isNotNull<A>(x: A): x is NonNullable<A> {
  return x!== null && x!== undefined;
}

isNotNull(x?.y?.z) && func(x.y.z) // nope it can evaluate to true/false but is also a type error
isNotNull(x?.y?.z) ? func(x.y.z) : null // nice, but TS has an issue with that, so doesn't work

// proper one:
const tmp = x?.y?.z;
isNotNull(tmp) ? func(tmp) : null // works
```

As you can see, there needs to be additional check before we can use the computation result as an argument of another function. That is bad. Also the fact `isNotNull(x?.y?.z) ? func(x.y.z) : null` is not working looks like TypeScipt bug. That is why I've created such - [optional chaining not works with type guards](https://github.com/microsoft/TypeScript/issues/34974).

In other words optional chaining has a problem with dealing with any computation which needs to be done on the result of it or in the middle of the chain. There is no possibility to chain custom expression working on the positive result of optional chaining. This always needs to be done by another conditions, and these condition have a different view on what the hell means no value by the Falsy/Truthy rules.

## Fixing the issue

This issue not exists in functional programming constructs like *Maybe* (known also as Optional), where it is possible to call function on positive result of the optional chain (via map or chain functions). What exactly optional chaining is missing is a [Functor](http://www.tomharding.me/2017/03/27/fantas-eel-and-specification-6/) behavior, but the problem is - there is no additional computation context where we could be having a Functor. `?.` can be considered as kind of `chain/flatMap` but in limited scope of object methods and properties accessing. So it is a `flatMap` where the choice is only get property functions, but still it is something.

`Maybe` is a sum type which has two value constructors - `Some<Value> | None`. In order to use new syntax of optional chaining, but have a power of *Maybe* we can do a neat trick. As we know that optional chaining treads `None` as `null | undefined`, that means that our *Maybe* could be doing the same. The second is - optional chaining works nicely with methods, as methods are just callable object properties. Taking these two, lets create implementation of *Maybe* which uses both things.

```ts
type None = null | undefined; // no value is represented like in optional chaining
type Maybe<ValueType> = Some<ValueType> | None;
```
Ok, so we share the same definition of empty value between our new construct and optional chaining. Now *Maybe* implementation. 

> **Caution** our Maybe will be an instance of Functor, but will not be an instance of Monad, as we will use optional chaining to gain chaining powers.

```ts
class Some<ValueType> {
  value: ValueType;
  constructor(value: ValueType) {
    this.value = value;
  }
  map<NextValueType>(f: (x: ValueType) => NextValueType): Some<NextValueType> {
    return new Some(f(this.value));
  }
  get() {
    return this.value; // just return plain data
  }
} 
type None = null | undefined;
type Maybe<ValueType> = Some<ValueType> | None;

// value constructor / alias on new Some
const some = <ValueType>(v: ValueType) => new Some(v);
```
Also take a look that TS automatically treads class definition as a type definition. So we have implementation and type in one language construct.

Now lets use this construct with optional chaining. I will use similar structure which I've presented in the previous examples, but with using of the new construct.
```ts
type NestedType = {
    y?: {
      z?: Maybe<number>  // number in optional context
    }
}

// version with using of our Maybe construct methods
function add1(x:NestedType) {
  return x?.y?.z?.map(z => z + 1).get()
}
add1({y: {z: some(1)}}) // result is 2
add1({y: {z: some(0)}}) // result is 1
add1({y: {}}) // result undefined
add1({}) // result is undefined

// compare to version without a Maybe and Functor features
function add1(x) {
  const v = x?.y?.z;
  if (isNotNull(v)) {
    return v + 1;
  }
  return null;
}

```

Conclusion. By some effort and using additional abstractions (Functor) it is possible to use optional chaining with functions and without dealing with additional conditions and implicit complexity. Of course as always there is a trade-off, and here this additional abstraction is a wrapper over standard plain data. But this abstraction gives us super powers of reusing functions with not optional arguments inside optional context.

Additional thoughts. Some of you have an issue that this article is kinda about Falsy/Truthy issues and not new operator issues. That was really not my intention. It's more about the whole, so how much problems we still have even after introduction of the operator, and main point is you cannot use it without additional conditions as it lacks possibility of mapping it's positive result.