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:
- Practical Guide to Fp‑ts
- Mostly adequate guide to FP
- The State monad
- fp-ts Learning Resources and fp-ts recipes
- Functors, Applicatives, And Monads In Pictures (in Haskell, but with pictures)
And here are some more interesting articles/posts about fp-ts:
- fp-ts and Even More Beautiful API Calls (w/ sum types!)
- Getting started with fp-ts: Reader, for dependency injection
- https://samhh.github.io/fp-ts-std/, dubbed "the missing pseudo-standard library"
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. Many things here decomplected, code is concise, extensible, and with static type checking. The tradeoff is the cost of learning a whole new functional API.
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)
- ← Previous post: Building cathedrals
- → Next post: Parameterize AWS Lambda with SSM Param Store
This blog is written by Marcel Krcah, an independent consultant for product-oriented software engineering. If you like what you read, sign up for my newsletter