Pragmatic types: types vs null

stereobooster - Aug 24 '18 - - Dev Community

Null References: The Billion Dollar Mistake

-- Tony Hoar, the inventor of a null reference. See his talk

What is null and why you should care?

Null represents an absence of value. For example, if you try to get value from an array (vector), there is a chance that value will be missing (if the array is empty). What system can do in that case?

  • throw exception (or return it, like they do in Go)
  • return value which represents an absence of value - null value

Also, null values used for uninitialized values, you need those to construct cyclic structures.

What is the issue

The main issue is when a null value is considered to be part of all types e.g. null is a valid number or valid string, but same time you can not apply any of operations for the given type to a null value.

"" + undefined
"undefined"

1 * undefined
NaN

Null value and JavaScript

Tony Hoar said that null reference is the billion dollar mistake. JavaScript doubles it by introducing two null values: null and undefined. I'm not sure why, I believe there is some historical reason, probably null was introduced to brand JS as close to Java as possible (the same as name choice).

null === undefined
false
null == undefined
true
1 * undefined
NaN
1 * null
0

Type-based solution

Flow and other modern type systems don't consider null (or undefined) as part of any "basic" type:

Flow:

let x:number = undefined;
                  ^ Cannot assign `undefined` to `x` because undefined [1] is incompatible with number [2].

TypeScript:

let x:number = undefined;
Type 'undefined' is not assignable to type 'number'.

also, it checks if the variable initialized or not

Flow:

let x:number;
x * 1;
^ Cannot perform arithmetic operation because uninitialized variable [1] is not a number.

TypeScript:

let x:number;
x * 1;
Variable 'x' is used before being assigned.

but be careful with Flow:

let x:number;
x + 1;
No errors!

TypeScript

let x:number;
x + 1;
Variable 'x' is used before being assigned.

Option type or how to represent the absence of value with types

The good way to represent null value is so called tagged* unions - imagine you have a collection of values of different type, if you will attach some tag to each value by each you can clearly differentiate one value from another you can safely mix it in one bag. So you mark all actual values with one tag and you have one special value with a tag which represents a null value.

You can imagine it like this (rough example, you don't do it like this in JS):

const taggedValues = [
  {
    type: "Some",
    value: 1
  },
  {
    type: "None"
  },
]

in ML languages you do not need to construct objects, you can make it with the help of types

type 'a option = None | Some of 'a

it can be roughly translated to Flow as

type None = void
type Some<a> = a
type Option<a> = None | Some<a>
// or simpler
type Option<a> = void | a
// even simpler - syntax sugar
type Option<a> = ?a

The native solution in Flow is called Maybe type.

TypeScript:

type Option<a> = void | a

If you use Option type, the system will make sure you do not apply any operation to the value unless you checked that the value is actually present.

let x:?number = 1;
x * 1;
^ Cannot perform arithmetic operation because null or undefined [1] is not a number.

let x:?number;
if (x != undefined) x * 1;
No errors!

TypeScript

let x:?number = 1;
x * 1;
No errors

but:

const t = (x:number|void) => x * 1;
The left-hand side of an arithmetic operation must be of type 'any', 'number' or an enum type.

Option type - is a safer alternative for a null value. (There is one more alternative - Maybe monad.)

Verbosity of the Option type

Before you will be able to use Option type value in any operation (that requires exact type) you need to prove that the value is actually there:

let x:?number;
// some code which touches x

// x can be number or null or undefined at this point of code
if (x != undefined) {
  // x only can be a number at this point of code,
  // otherwise we will not get into this branch.
  // We can say that if condition proves that inside of this scope x is number.
  x * 1;
}

this approach can be verbose, that is why I try to use Option type only if it is required. For example, to describe the state of the form:

type State = {
  name?: string,
  age?: number
};
// initial state of the form
let state: State = {};
// user filled in first field
state = { name: 'Abc' };
// user filled in second field
state = { name: 'Abc', age: 20 };

but as soon as the user submits (and all fields required to be filled in, before the user can submit), we can use stricter types:

type User = {
  name: string,
  age: number
};
const onSubmit = (user: User) => { /* code */ };

This helps to fight with verbosity

Word of caution

I advertised Option type so much, but there is one caveat in Option type impelemntation in Flow and TypeScript:

const a: Array<{c:1}> = [];
const b = a[0]
const d = b.c;
No errors!
const e: {[key: string]: {c:1}} = {};
const f = e.f;
f.c;
No errors!

It will not catch errors here! Keep this in mind.

This post is part of the series. Follow me on twitter and github.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .