DEV Community

stereobooster
stereobooster

Posted on

Pragmatic types: types vs null

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.

Top comments (5)

Collapse
 
stereobooster profile image
stereobooster

Not sure I get your argument. Variables exist in both cases, but they have no value. (I know that from machine point of view they have two distinct values JSVAL_NULL and JSVAL_VOID)

And this is what null is for - to represent absence of value. We have variable, but it doesn't hold any value. If you want to read more about historical reasons why JS have both null and undefined you can read this post by Dr. Axel Rauschmayer

 
stereobooster profile image
stereobooster • Edited

To make sure we use the same terminology

A variable that is undefined does not exist

A variable that doesn't exist is an error of a different kind, this is not the same as an uninitialized variable. For example JS:

a
VM54:1 Uncaught ReferenceError: a is not defined
    at <anonymous>:1:1
(anonymous) @ VM54:1

Flow:

a   
^ Cannot resolve name `a`.

TypeScript:

a
Cannot find the name 'a'.

On the other hand, an uninitialized variable is an existing variable which doesn't have value. It can have no value for two reasons:

  • it wasn't assigned a value at the moment of creation, and will be assigned later, like let a;
  • it was assigned with the result of an operation which can return null-value, like let a = array[0]; (undefined in case of JS)

In my opinion, the destination between null and undefined is over-complication. They both represent the absence of value. undefined was introduced for historical reasons, I believe this is just an oversight in the language design (see the post by Dr. Axel Rauschmayer). The same way as Tony Hoar says that null was a mistake in AlgolW design.

Collapse
 
codevault profile image
Sergiu Mureşan

I straight up never use the null value in JavaScript vanilla. It just gives me a lot of headaches.