Lodash chaining alternative

munawwar

Munawwar

Posted on July 15, 2020

Lodash chaining alternative

Those of you dealing with data transforms/manipulations for charting/dashboards/whatever, need no introduction to Lodash library, and have been using it happily on the backend, frontend etc

The problem

However there is one method Lodash has that's so useful but has its performance implications on the frontend.. namely chain().
On the frontend if you naively import chain the entire lodash library will end up in your bundle.. and the entire lodash library isn't small. Backend code doesn't care about some extra bloat.

That's sad. chain is very useful and I would like to use chaining on the frontend without having to take a performance hit. So.. what is the solution?

What does Google say?

Googling around you would see many suggestions to use lodash/fp's flow() method. You can see the code from this 2016 post

import map from "lodash/fp/map";
import flatten from "lodash/fp/flatten";
import sortBy from "lodash/fp/sortBy";
import flow from "lodash/fp/flow";
flow(
  map(x => [x, x*2]),
  flatten,
  sortBy(x => x) 
)([1,2,3]);
Enter fullscreen mode Exit fullscreen mode

It works.. it keeps the bundle size small and gives you chaining capability.

But there is something bothersome about that code...

_.chain begins with the data you need to manipulate and then you call the transforms.. whereas flow() begins with the transforms and ends with the data you want to manipulate. This isn't natural to read. It needs to be flipped around.

[From flow()'s perspective it is built as intended. flow is potentially built for reuse. Fine. However we still miss a closer alternative to chain.]

Alternative solution

My ideal syntax would be the following (for the same code sample from above):

chain([1,2,3])
  (map, x => [x, x*2])
  (flatten)
  (sortBy, x => x)
  ();
Enter fullscreen mode Exit fullscreen mode

However most linter configurations would complain about the indented parentheses. So we need a dummy function and a .value() to break out of the chain (like lodash already does)

chain([1,2,3])
  .fn(map, x => [x, x*2])
  .fn(flatten)
  .fn(sortBy, x => x)
  .value();
Enter fullscreen mode Exit fullscreen mode

Overall if you squint your eyes and ignore the .fn()s, then it looks very similar to lodash's _.chain syntax. And there is a way to implement this. I'll dive straight into the implementation which is small and probably don't need too much explanation:

function chain(value) {
  return {
    fn: (func, ...args) => chain(func(value, ...args)),
    value: () => value,
  };
}
Enter fullscreen mode Exit fullscreen mode

This implementation brings some new opportunity considering how generic the approach is.

The function doesn't know anything about lodash. It takes in any function. So you can write custom functions or use Math.* or Object.* functions

chain({prop: 2, fallback: 1})
  .fn((obj) => obj.prop || obj.fallback)
  .fn(Math.pow, 2)
  .value(); // result = 4
Enter fullscreen mode Exit fullscreen mode

Improvement

With a slight modification, we can make it call any function on result objects.

Which mean for arrays, we can use native array map, filter etc, and we don't really need to use lodash's functions there. We should be able to do something like the following (using same example from before):

chain([1,2,3])
  .fn('map', x => [x, x*2])
  // ... blah
  .value();
Enter fullscreen mode Exit fullscreen mode

Instead of passing the function here we put a name of the method to be invoked from the intermediate result object/array. The implementation of fn will change to the following:

    fn(func, ...args) {
      if (typeof func === 'function') {
        return chain(func(value, ...args));
      }
      return chain(value[func](...args));
    },
Enter fullscreen mode Exit fullscreen mode

We can further improve this to handle promises / async functions:

    fn(func, ...args) {
      if (value instanceof Promise) {
        return chain(value.then((result) => {
          if (typeof func === 'string') {
            return result[func](...args);
          }
          return func(result, ...args);
        }));
      }
      if (typeof func === 'string') {
        return chain(value[func](...args));
      }
      return chain(func(value, ...args));
    },
Enter fullscreen mode Exit fullscreen mode

And then it can be used as follows:

const dates = await chain(Promise.resolve([1, 2, 3]))
  // with method name
  .fn('map', x => x.toString())
  // with functions
  .fn((arr) => arr.map(x => new Date(x)))
  .value();
Enter fullscreen mode Exit fullscreen mode

I believe this is an improvement to the popular approaches suggested out there on the interwebz. Check it out, try it out.. criticism welcome.

That's all folks. Hope you liked my short, hastily written post.

Full code below:

/** @type {import('./chain').default} */
export default function chain(value) {
  return {
    fn(func, ...args) {
      if (value instanceof Promise) {
        return chain(value.then((result) => {
          if (typeof func === 'string') {
            return result[func](...args);
          }
          return func(result, ...args);
        }));
      }
      if (typeof func === 'string') {
        return chain(value[func](...args));
      }
      return chain(func(value, ...args));
    },
    value() {
      return value;
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

chain.d.ts

declare type ParametersExceptFirst<F> = F extends (arg0: any, ...rest: infer P) => any ? P : never;
declare type MethodsOf<T> = {
  [K in keyof T]: T[K] extends ((...arg: any[]) => any) ? T[K]: never;
};

export default function chain<V>(val: V): {
  fn<Func extends (v: Awaited<V>, ...args: any[]) => any>(func: Func, ...args: ParametersExceptFirst<Func>): ReturnType<typeof chain<ReturnType<Func>>>;
  fn<Name extends keyof MethodsOf<Awaited<V>>>(
    func: Name,
    ...args: Awaited<V>[Name] extends ((...args: infer P) => any) ? P : never
  ): ReturnType<typeof chain<ReturnType<MethodsOf<Awaited<V>>[Name]>>>;
  value(): V;
};
export {};
Enter fullscreen mode Exit fullscreen mode

EDIT 2022: Added type definition and improved implementation
EDIT 2024: Added promise / async function handling

💖 💪 🙅 🚩
munawwar
Munawwar

Posted on July 15, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related