DEV Community

loading...
Cover image for Converting Lodash to fp-ts

Converting Lodash to fp-ts

Timothy Ecklund
Engineering is hard, let's get better at it together!
・3 min read

TLDR; I have started a project to provide examples of how to convert from Lodash to fp-ts and I could use your help! Please consider chipping in, all PRs are welcome!

Lodash is the single most downloaded package on npm. It deserves its place at the top - it provides a massive suite of functionality that is performant and has a clear, consistent interface. Lodash is, without a doubt, a fantastic Javascript library.

However, Lodash was written before the advent of Typescript and has significant holes when it comes to typed functional programming. Let me hit you with an example:

const log = console.log // just to make things a little nicer to read

const obj = { 'a': [{ 'b': { 'c': 3 } }] }
const result =  _.get(obj, 'a[0].b.c')
const ohno = _.get(obj, 'a[0].b.d')
log(result) // 3
log(ohno) // undefined
Enter fullscreen mode Exit fullscreen mode

What type is result? Why, it's the any type! Not only are we missing type information on the result, we are also missing type information on the path we provided - if someone renames c to d we won't know until it gets all the way to production and explodes. On top of that we have to remember to check for undefined everywhere it might exist. Hope you never forget!

dogsandcats

There is a better way! Let's look at how to do this using libraries that were designed from the ground up for typescript (fp-ts and monocle-ts):

import * as Op from 'monocle-ts/lib/Optional'

const getC = pipe(
  Op.id<{ a: readonly { b: { c: number } }[] }>(),
  Op.prop('a'),
  Op.index(0),
  Op.prop('b'),
  Op.prop('c'),
  opt => opt.getOption,
)
log(getC(obj)) // { _tag: 'Some', value: 3 }
Enter fullscreen mode Exit fullscreen mode

Aw yeah. This is a technique known as Optics and it provides type safety through and through. Notice that we are providing a type with id - any calls to prop that don't align with the type will error. Finally, we're safe from Dave a few desks down who is constantly renaming things. We also have a strong return type - Option<number>. Option will force us to remember to add error handling in case our object is malformed, and number because we know that c is a number. phew

Here's another example:

var mutable = {a: 0, b: 2}
log(_.assign(mutable, {a: 1, c: 3}))
log(mutable)
// { a: 1, b: 2, c: 3 }
// { a: 1, b: 2, c: 3 }
Enter fullscreen mode Exit fullscreen mode

Mutation! Noooooo! :(
mutant

Let's try again, this time with a library that is consistently immutable:

import {merge} from 'fp-ts-std/Record'

var mutable = {a: 0, b: 2}
log(merge(mutable)({a: 1, c: 3}))
log(mutable)
// { a: 1, b: 2, c: 3 }
// { a: 0, b: 2 }
Enter fullscreen mode Exit fullscreen mode

Oh thank goodness, we're safe.

safe

In my opinion, the biggest hurdle to widespread adoption of fp-ts is a lack of good examples. Almost everyone is familiar with Lodash - why not provide a set of examples that would help everyone transition?

Well I've started doing just that. I hope as people see the conversion is simple, and the benefits provided are significant, fp-ts will become even more widespread. Wouldn't that be a wonderful world?

Working through all the Lodash functions can take a long time, however, and I am (gasp) sometimes wrong. If you are reading this and have a few minutes, please take a crack at helping me with this project. PRs are very welcome!

Discussion (3)

Collapse
cdimitroulas profile image
Christos Dimitroulas

Here's an example I used in a PR at work the other day to advocate against using lodash now that we've moved to Typescript:

import * as A from "fp-ts/lib/ReadonlyArray";
import { flow } from "fp-ts/lib/function";
import fp from "lodash/fp";
// This compiles no problem! Somehow lodash is happy to compose a function
// which returns a number with a function which accepts `null | { name: string }`
// myFn is typed as `(...args: any[]) => any` as well, which can cause other
// problems later down the line
const myFn = fp.pipe(
  (x: number[]) => x.reduce((accum, val) => accum + val, 0),
  (banana: null | { name: string }) =>
    "haha I can compose random functions together" + JSON.stringify(banana),
);
// This errors with `Type 'number' is not assignable to type '{ name: string; } | null'`
const myFn1 = flow(
  (x: number[]) => x.reduce((accum, val) => accum + val, 0),
  (banana: null | { name: string }) =>
    "haha I can compose random functions together" + JSON.stringify(banana),
);
Enter fullscreen mode Exit fullscreen mode
Collapse
anthonyjoeseph profile image
Anthony G

Great article! Here are another couple simple examples showing the advantages of fp-ts's strong type paradigms:

import sortBy from 'lodash/sortBy'
import { flow, map, takeRight } from 'lodash/fp'
import { pipe } from 'fp-ts/function'
import * as A from 'fp-ts/ReadonlyArray'
import * as Ord from 'fp-ts/Ord'
import * as N from 'fp-ts/number'

interface Result { rank: number; value: string }
declare const results: Result[]

const lodashSort = sortBy(results, 'ranck') // <-- no type error
const fptsSort = pipe(
  results,
  A.sort(pipe(
    N.Ord,
    Ord.contramap((b: Result) => b.ranck) // <-- type error
  ))
)

const lodashFlow = flow(
  map((r) => r.rank + 3), // <-- type error: Object is of type 'unknown'
  takeRight(2),
)(results)
const fptsPipe = pipe(
  results,
  A.map((r) => r.rank + 3), // <-- no type error
  A.takeRight(2)
)
Enter fullscreen mode Exit fullscreen mode
Collapse
patroza profile image
Patrick Roza

log(A.filter(x => x ? true : false)([0, 1, false, 2, '', 3]))
->
log(A.filter(Boolean)([0, 1, false, 2, '', 3]))
or

const compact = A.filter(Boolean)
log(compact([0, 1, false, 2, '', 3])) 
Enter fullscreen mode Exit fullscreen mode

instead of Boolean, one that also narrows the type:
function isTruthy<T>(item: T | null): item is T { return Boolean(item) }

Forem Open with the Forem app