Getting started with functional programming in Typescript

I'm building a Typescript app, and I have reached a level of accidental complexity that I'm not able to remove. I believe a paradigm change is needed to cross the chasm. Namely, to introduce parts of functional programming (FP). Now, to start with functional programming in Typescript, I need two things: a library to use as a backbone and resources to learn from. Here's what I've found so far.

Libraries

Based on what I've seen, the most popular libraries are Lodash, Ramda (Rambda, Rambdax) and fp-ts. It seems that Lodash and Ramda and Rambda were built for JS first, with Typescript types added later, while fp-ts was built for Typescript first. One can see that in the interfaces:

# rambdax 
import { pipeAsync } from 'rambdax'
await pipeAsync(foo)('hello')   
// rambdax's pipeAsync is curried, 
// the compiler has no way to check if foo is compatible with 'hello'

# fp-ts
import { pipe } from 'fp-ts/function'
pipe('hello', foo) 
// fp-ts's pipe is not curried, 
// the compiler can check the compatibility of foo with 'hello'

Interestingly, fp-ts also contains Task that can replace Promise for better type safety. See Should I use fp-ts Task? post.

I'm intrigued by fp-ts.

Resources

I've found these posts to be excellent resources for starting with FP:

And here are some more teasers for fp-ts:

fp-ts example

Here's a non-trivial example (source) of fp-ts that includes async execution against a REST API, validation, and error handling. I liked the code: many things decomplected, and code is concise, extensible, and with static type checking.

import axios, { AxiosResponse } from 'axios'
import { flatten, map } from 'fp-ts/lib/Array'
import * as TE from 'fp-ts/lib/TaskEither'
import * as E from 'fp-ts/lib/Either'
import * as T from 'fp-ts/lib/Task'
import { sequenceT } from 'fp-ts/lib/Apply'
import { pipe } from 'fp-ts/lib/pipeable'
import { flow } from 'fp-ts/lib/function'
import { failure } from 'io-ts/lib/PathReporter'
import * as t from 'io-ts'

//create a schema to load our user data into
const users = t.type({
  data: t.array(t.type({
    first_name: t.string
  }))
});
type Users = t.TypeOf<typeof users>

//schema to hold the deepest of answers
const answer = t.type({
  ans: t.number
});

//Convert our api call to a TaskEither
const httpGet = (url:string) => TE.tryCatch<Error, AxiosResponse>(
  () => axios.get(url),
  reason => new Error(String(reason))
)

//function to decode an unknown into an A
const decodeWith = <A>(decoder: t.Decoder<unknown, A>) =>
  flow(
    decoder.decode,
    E.mapLeft(errors => new Error(failure(errors).join('\n'))),
    TE.fromEither
  )

//takes a url and a decoder and gives you back an Either<Error, A>
const getFromUrl = <A>(url:string, codec:t.Decoder<unknown, A>) => pipe(
  httpGet(url),
  TE.map(x => x.data),
  TE.chain(decodeWith(codec))
);

const getAnswer = pipe(
  TE.right({ans: 42}),
  TE.chain(decodeWith(answer))
)

const apiUrl = (page:number) => `https://reqres.in/api/users?page=${page}`

const smashUsersTogether = (users1:Users, users2:Users) =>
  pipe(flatten([users1.data, users2.data]), map(item => item.first_name))

const runProgram = pipe(
  sequenceT(TE.taskEither)(
    getAnswer, 
    getFromUrl(apiUrl(1), users), 
    getFromUrl(apiUrl(2), users)
  ),
  TE.fold(
    (errors) => T.of(errors.message),
    ([ans, users1, users2]) => T.of(
      smashUsersTogether(users1, users2).join(",") 
      + `\nThe answer was ${ans.ans} for all of you`),
  )
)();

runProgram.then(console.log)


Would you like to connect? Subscribe via email or RSS , or follow me on Twitter!