Option
APIOption type and functions as replacement for
nullorundefined
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>
Always use Option type when possible
-
Avoid using
null// ✓ GOOD
const someOptionFunc = () => Option.from(someNullableFunc()); // null -> undefined -
Option<V>is more expressive thanV | undefinedand more readable, especially when combining with more union type
Use precise and meaningful functions
- Prefer
Option.map/Option.andThen/Option.orElsewhen mapping anOptionto anOption - 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.
Method 1: Option.isNone / Option.isSome (Recommended)
- ✓ 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)
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) ?
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
nullandundefined - Every access to a property or array would have to be converted from
undefinedornulltoNone|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
undefinedtonull
SOLUTION 3 : undefined as None :
PROS :
- array and property access are already well typed
typeof undefined == 'undefined'
CONS :
undefineddoes not exist in JSON
Why choose the name Option over Maybe ?
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.