Skip to content

Utilities for composable approach to handle null and undefined

License

Notifications You must be signed in to change notification settings

YuriiOstapchuk/fully-optional

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

51 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fully-optional

npm package Build Status Coverage Status dependencies Status Code style

Handle null and undefined in safe and composable way.

Reasoning

When dealing with nullability in Typescript you need to create a union type T | undefined and then rely on Typescript control flow analysis to make sure you don't use undefined where you want something else

However this approach has its flaws:

  • The code is imperative, you do not immediately see the business logic or data manipulation
  • The code can quickly become a mess when you nest if statements or check multiple values for null
type Data = {
  data?: {
    date?: string;
  };
};

declare const getData: () => Data | undefined;

declare const parseDate: (date: string) => Date | undefined;

Default approach

const f = () => {
  const data = getData();

  let date: Date | undefined;

  if (data && data.data) {
    const dateString = data.data.date;

    if (dateString) {
      date = parseDate(dateString);
    }
  }

  date = date || new Date();

  return date.toLocaleDateString();
};

Using fully-optional

import { flow } from 'lodash/fp';
import { bind, withDefaultLazy } from 'fully-optional';

const f = flow(
  getData,
  bind((data) => data.data),
  bind((data) => data.date),
  bind(parseDate),
  withDefaultLazy(() => new Date()),
  (date) => date.toLocaleDateString(),
);

This library provides an abstraction over manual handling of null values with a concise and composable API

There are, however, other approaches to solve null problem with composability in mind: Maybe or Option monads.

So why should you use fully-optional instead of a Maybe monad?

  • When using Maybes you introduce a new wrapper datatype that does not integrate well into an existing javascript ecosystem

  • There is no need to introduce a new way to handle null values when we have a good standart solution with union types

  • Union types scale better than Maybe monad.

    For example you have a function

    declare const f: (value: number) => Maybe<T>;

    When you refactor it's type to

    declare const f: (value: number) => T;

    You will break all the callers of this function, but you will not break them by changing the return type from T | undefined to T

Install

npm install fully-optional
yarn add fully-optional

Usage

import { bind } from 'fully-optional';

declare const a: string | undefined;

bind(a, parseInt); // inferred type number | undefined

All functions are also curried data-last to allow composition

import { flow } from 'lodash/fp';
import { bind } from 'fully-optional';

type X = {
  a?: {
    b?: string;
  };
};

declare const f: (...args: any[]) => X | undefined;

const r = flow(
  f,
  bind((e) => e.a),
  bind((e) => e.b),
  bind(parseInt),
); // inferred type number | undefined

API

all

Apply a function to an array of values if all of them are not null or undefined

declare const arr: [string | undefined, number | undefined];

all(arr, ([s, n]) => parseInt(s) * n); // number | undefined

bind

Apply a function to a value if it is not null or undefined

declare const a: string | undefined;

bind(a, (e) => e.toUpperCase()); // string | undefined

isEmpty

Check if value is null or undefined

isEmpty(a);

isNotEmpty

Check if value is not null or undefined

isNotEmpty(a);

match

Give two functions to handle both empty and non empty cases

declare const a: string | undefined;

match(a, {
  some: (e) => e.toUpperCase(),
  none: () => '',
});

withDefault

Return default value if the argument is null or undefined

declare const a: string | undefined;

withDefault(a, '');

withDefaultLazy

Calculate and return default value if the argument is null or undefined

declare const a: string | undefined;
declare const expensiveDefaultValue: () => string;

withDefaultLazy(a, expensiveDefaultValue);

Contributing

Pull requests are welcome.
Please make sure to update tests as appropriate.

License

MIT