Skip to main content

Option

Option type and functions as replacement for null or undefined

API

Overview

An Option<V> is either a value of type V or nothing. An empty value is represented by Option.None and a defined value by Option.Some(...). Internally, Option.None is undefined.

import { Option } from '@w5s/core';

// ┌─ Type of the optional value
type ValueOption = Option<Value>
tip

Always use Option type when possible

  • Avoid using null

    // ✓ GOOD
    const someOptionFunc = () => Option.from(someNullableFunc()); // null -> undefined
  • Option<V> is more expressive than V | undefined and more readable, especially when combining with more union type

tip

Use precise and meaningful functions

  • Prefer Option.map / Option.andThen / Option.orElse when mapping an Option to an Option
  • Prefer ternary operators over if / else
// ✓ OK
const myFunc = <V>(option: Option<V>) => Option.map(option, (value) => /* ... */);

// = OK with caution
const myFunc = <V>(option: Option<V>) => Option.isNone(option) ? /* ... */ : /* ... */;
// ⚠️ Be careful to return the same type in both cases

// ⤫ BAD
const myFunc = <V>(option: Option<V>) => {
if (Option.isNone(option)) {
return /* ... */ // Risk of returning a different type on both branches
}
return /* ... */
};

Chaining

Use Option.map, Option.andThen and Option.orElse to transform a given Option<T> to Option<U>.

Example :

function parseNumber(expression: string): Option<number> {
const result = parseInt(expression, 10)
// Parse a number from a string
return Number.isNaN(result) ? Option.None : Option.Some(result);
}

function program(expression: string) {
// Convert string to number
return parseNumber(expression) // Option<number>
// Divide by 2 if not None
|> Option.map(#, (_) => _ / 2) // Option<number>
// Returns None if previous result was 0, let unchanged otherwise
|> Option.andThen(#, (_) => _ === 0 ? Option.None : Option.Some(_)); // Option<number>
// Format to string if not None
|> Option.map(#, (_) => `Divided by 2 : ${_}`); // Option<string>
// Handle Option.None
|> Option.orElse(#, () => Option.Some(`Expression is not valid`)); // Option<string>
}

Matching on values

Use matching to transform Option value to any type of value.

note
  • ✓ Best Expressiveness
  • ✓ Good performances
import { Option } from '@w5s/core';

const optionToString = <V>(option: Option<V>): string => Option.isSome(option) ? `Some(${v})` : 'None');
optionToString(Option.Some(1));// 'Some(1)'
optionToString(Option.None);// 'None'

Method 2: === undefined / !== undefined (i.e. inlining isNone / isSome)

note

Not recommended for an application, but only for a third party library.

  • ⚠️ Low expressiveness
  • ✓ Highest performances
  • ✓ No module load overhead
import type { Option } from '@w5s/core';

const optionToString = <V>(option: Option<V>): string => option === undefined ? `Some(${v})` : 'None';

FAQ

Why choose undefined instead of null or a variant object (like fp-ts) ?

SOLUTION 1 : Tagged variant { _: 'None' } | { _: 'Some', value, } :

PROS :

  • Generic pattern matching

CONS :

  • Creates a third "nullable" representation after null and undefined
  • Every access to a property or array would have to be converted from undefined or null to None|Some()

SOLUTION 2 : null as None :

PROS :

  • JSON friendly

CONS :

  • `typeof null == 'object'``
  • Every access to a property or array would have to be converted from undefined to null

SOLUTION 3 : undefined as None :

PROS :

  • array and property access are already well typed
  • typeof undefined == 'undefined'

CONS :

  • undefined does not exist in JSON

Why choose the name Option over Maybe ?

It is a matter of preference. Rust uses Option, Haskell uses Maybe. Generally speaking, W5S packages naming tends to be often aligned with the Rust naming when no ECMA equivalent exists.