unsplash / pipe-ts

34 stars 4 forks source link

Suggestion: pipe with early exit on null or undefined #12

Closed wassim-k closed 2 years ago

wassim-k commented 2 years ago

Hi,

Great work on this library, it's very clean and simple. I wanted to suggest a function that I believe can provide a lot of value to the library without comprimising its simplicity. The function is rail/railWith, it's similar to pipe except it automatically exits when it first encounters a null | undefined value.

It basically allows developers to rewrite this:

pipeWith(
  a,
  b => b !== undefined ? calcB(b) : undefined,
  c => c !== null? calcC(c) : null,
  d => d !== null && d !== undefined ? calcD(d) : d
);

into this:

railWith(
  a,
  b => calcB(b),
  c => calcC(c),
  d => calcD(d)
);

The name of the function can be changed into whatever, but I chose it based very loosely on rail road programming principles.

Here's an example implementation:


import { pipe } from 'pipe-ts';

type UnknownFunction = (...params: Array<unknown>) => unknown;
type ExtractNullOrUndefined<T> = Extract<T, null | undefined>;

export function railWith<A, B>(a: A, ab: (this: void, a: NonNullable<A>) => B): B | ExtractNullOrUndefined<A | B>;
export function railWith<A, B, C>(
    a: A,
    ab: (this: void, a: NonNullable<A>) => B,
    bc: (this: void, b: NonNullable<B>) => C,
): C | ExtractNullOrUndefined<A | B | C>;
export function railWith<A, B, C, D>(
    a: A,
    ab: (this: void, a: NonNullable<A>) => B,
    bc: (this: void, b: NonNullable<B>) => C,
    cd: (this: void, c: NonNullable<C>) => D,
): D | ExtractNullOrUndefined<A | B | C | D>;
export function railWith<A, B, C, D, E>(
    a: A,
    ab: (this: void, a: NonNullable<A>) => B,
    bc: (this: void, b: NonNullable<B>) => C,
    cd: (this: void, c: NonNullable<C>) => D,
    de: (this: void, d: NonNullable<D>) => E,
): E | ExtractNullOrUndefined<A | B | C | D | E>;
export function railWith<A, B, C, D, E, F>(
    a: A,
    ab: (this: void, a: NonNullable<A>) => B,
    bc: (this: void, b: NonNullable<B>) => C,
    cd: (this: void, c: NonNullable<C>) => D,
    de: (this: void, d: NonNullable<D>) => E,
    ef: (this: void, e: NonNullable<E>) => F,
): F | ExtractNullOrUndefined<A | B | C | D | E | F>;

...
...
...

export function railWith(value: unknown, ...fns: Array<UnknownFunction>): unknown {
    return pipe(
        // Our overloads do not currently support arrays
        // @ts-ignore
        ...fns.map(fn => v => v !== null && v !== undefined ? fn(v) : v)
    )(value);
}

If you see the value in it too, let me know and I'll be happy to polish the solution and raise a PR.

Keep up the good work 👍

wassim-k commented 2 years ago

Closing, as I managed to achieve the same result with the use of:

export function propagateNull<T, R>(fn: (value: NonNullable<T>) => R) {
    return (v: T): R | Extract<T, null | undefined> => isNonNull(v) ? fn(v as NonNullable<T>) : v;
}

pipeWith(
  a,
  propagateNull(b => calcB(b)),
  propagateNull(c => calcC(c)),
  propagateNull(d => calcD(d))
);