fp-toolkit v3.1.0
fp-toolkit ⚒️
Table of Contents
- Introduction
- Design goals
- Modules overview
- Comparison to fp-ts
- Tips for debugging
- Contributing
- Local development
Introduction
This library is for TypeScript developers who want to get $#!7 done using functional programming techniques but without all the esoteric ivory tower mumbo jumbo that usually comes along with that. This is not an attempt to make TypeScript into something that it isn't meant to be; we're not trying to shoehorn everything from a "pure" functional language like PureScript or Haskell into TypeScript. This library is much more closely aligned with F# in terms of design. It is "functional first," but allows developers to take and use the best parts of both the functional and imperative paradigms.
This library focuses on a select handful of types taken from the functional paradigm that are incredibly useful for solving real-world programming problems. It doesn't care about monoids or semigroups, applicatives or endofunctors.
TL;DR — If you want category theory, this is not the library for you.
Design goals
- Clear documentation & lots of examples
- Make the type system work for us, not the other way around
- No esoteric names or functional programming mumbo jumbo
- No category theory
- An API that's as simple as possible but no simpler
- Focus on function purity and data immutability
- Consistent API across modules
Modules overview
This library aims to provide a set of types and functions that help to model real-world programming problems in a type-safe, concise, readable, and testable way:
Function composition
The function composition module exposes pipe for right-to-left pipelining of values through a set of functions, and it exposes flow for right-to-left function composition.
Array
The Array module is a set of useful, type-safe functions for working with readonly arrays using functional composition/pipelining. It offers functions like:
uniqsortBygroupByuniontakeskip- ...and many more!
NonEmptyArray
The NonEmptyArray module is a set of useful, type-safe functions for working with readonly arrays that contain at least one element. It offers additional functions on top of the Array module like:
rangedestructmakefirst- ...and more!
Async
The Async module helps you model and manage asynchronous workflows. Best of all, Async computations are Lazy, which means no work will start until you explicitly ask it to! The Async module offers:
sequential—execute a collection ofAsynccomputations in series and collect the resultsparallel—execute a collection ofAsynccomputations in parallel and collect the resultsdelaybind—chainAsynccomputations together, one after the othertee—for easy debugging- ...and more!
Result
The Result module is a simple, predictable, and type-safe way to model the outcome of an operation that can fail. Results come in handy in tons of situations: network calls, parsing unsafe input, DOM updates that may fail to apply, etc. The Result module offers:
match—get readable pattern matching semanticstryCatchtee&teeErr—for easy debugging- ...and many more functions!
AsyncResult
The AsyncResult module helps you model in a predictable, type-safe way any kind of async operation that can fail. (It's just the Async type with a more specific "inner value.")
tryCatch—to keep exception handling at the boundaries of your appmatch—pattern match against the innerResultbind—for chaining multiple async operations that can fail together such that they only happen if each preceding operation succeeds- ...and more!
Option
The ubuqituos Option<T> type. For those of you who are sick of null reference exceptions and want a way to model optional values that forces you to deal with the possibility of null, while retaining the fluidity of function composition pipelining. This module gives you:
ofNullishtoNullishfilterrefinedefaultValuedefaultWithmap&bind- ...and more!
Enums
The Enums module offers a way to work with enums in a more ergonomic way without the overhead of having to think about the nuances of the built-in enums that TypeScript provides. Use the enumOf function and you get for free:
match&matchOrElsefunctions for exhaustive pattern matching- an automagical
parsefunction - an array of all valid
values
Variants
The Variants module offers a much more ergonomic way to work with non-generic discriminated unions in TypeScript than you get out-of-the-box. Use a simple object describing each union case and the data it holds and you get a generated union type, constructor functions, and exhaustive/partial pattern matching functions for free!
If you're looking for something more full-featured (and also way more complex), check out the amazing variant library.
Deferred & DeferredResult
The Deferred and DeferredResult types are a kind of analogy to the Async and AsyncResult types. The Async* types model the work itself; the Deferred* types model the state of the ongoing async operation. These two types are incredibly useful in Redux reducer functions (or vanilla React useReducer hooks) because they can succinctly model exactly and only the valid states of an asynchronous operation. And they do that without "flag soup" of a bunch of loosely related boolean flags that are implicitly related to each other.
matchmatchOrElse- ...and more!
EqualityComparer & OrderingComparer
These modules make structural equality and decidable ordering of elements in TypeScript much easier. Easily describe how you would like types to be compared using functions like:
ofStruct—effortlessly define the structural equality for an object typederiveFrom—derive an equality or ordering comparer from one you already havegetComposite—combine ordering comparers using a "and then by ..., and then by..." approach- ...and more!
function
This module gives you two useful functions to make debugging easier in a functional pipelineing paradigm:
teeteeAsync
Map
Work with Map data structures in a type-safe and immutable way. Offers helpful utilities like:
iterreducefilter- ...and many more!
Nullable
Work with types that may be null | undefined in a fluid way. The nullish-coalescing ?? and elvis ?. operators only get you so far, because they require that you use a method-chaining approach. Using function pipelining with the Nullable module you can use any function you write with things like:
mapbinddefaultWith- ...and more!
String
A bunch of useful functions for working with strings in a functional paradigm.
trimcapitalizesplit- ...and more!
Comparison to fp-ts
This library was heavily inspired by fp-ts. fp-ts is a phenomenal library if what you want to do is pure functional programming that is heavily influenced by category theory.
If you have done more with fp-ts than a pet project, you'll know that as soon as you peek under the covers, you're greeted with all kinds of esoteric language: there is higher-kinded polymorphism, kleisli composition, instances of applicatives, monoids, monads, semigroups, oh my! 😱 Moreover, there are SO. MANY. MODULES. The API surface is enormous! (And that completeness is intended to be a feature for users, not a bug.)
While fp-ts is a fantastic library for its own purpose, its documentation is sparse and its API is huge, which makes it have an incredibly high learning curve. Many of the names chosen for functions are esoteric. In sum, it was written for power users.
fp-toolkit, on the other hand, is designed to be much more minimal. No category theory. No enormous API surface area to learn and master. We just kept the bits that are really useful for making real-world applications.
And, with common-sense names, documentation, and examples for nearly every function, the learning curve should be greatly reduced for onboarding even developers who are not familiar with functional programming!
Tips for debugging
One complaint that is frequently lodged against functionally-oriented code is that it is harder to debug. That complain is not entirely without merit—but it is also easily avoidable! When using functional pipelining and left-to-right composition, the easiest way to get access to and debug intermediate values is to use tee functions.
Tee and crumpets
The tee function gets its name from visualizing a literal plumbing pipeline:
|
├ <- This is the tee
|You can imagine the "flow" (of data, in this case) going downwards. The tee "splits off" from that main branch so that you can do something else with the data flowing through the pipeline.
Debugging with tee
Being able to "branch off" means that we can use tee to do things like log intermediate values. Consider this basic example just using the plain tee function from the function module.
import { pipe, tee, String, Array } from "fp-toolkit"
const x = pipe(
"cheese",
tee(console.log), // logs "cheese"
String.split(""),
tee(console.log), // logs ["c", "h", "e", "e", "s", "e"]
Array.reverse,
tee(console.log), // logs ["e", "s", "e", "e", "h", "c"]
Array.head,
tee(console.log) // logs `Option.some("e")`
) // => `Option.some("e")`As you can see, using tee gives you access to the value being passed through the function pipeline without affecting that value as it's being passed through. This means you can execute any arbitrary side effect against the value as it's being passed through.
Alternatively, if you wanted to open up an intermediate value to breakpoint debugging, you could pass a function with a { } body. This example uses teeAsync just to demonstrate:
import { pipe, flow, teeAsync, String, Array } from "fp-toolkit"
// curried version of `.then`, for illustrative purposes only
const mapPromise =
<A, B>(f: (a: A) => B) =>
(promise: Promise<A>): Promise<B> =>
promise.then(f)
// assume we have some promise representing an API response
declare const apiResponse: Promise<string>
const x = await pipe(
apiResponse,
teeAsync(r => {
// r is something like "status: ok"
console.log(r) // accessible for breakpoint debugging
}),
mapPromise(flow(String.split(" "), Array.head, String.capitalize)),
teeAsync(firstWordCapitalized => {
// "Status:"
console.log(firstWordCapitalized) // accessible for breakpoint debugging
})
) // => resolves to something like "Status:"teeAsync will log the inner value of the promise once it resolves, whereas if you try to use plain tee with promises, you will end up just having access to the Promise object (which may actually be useful in some scenarios). Note again, using tee* functions does not change the value being passed through the rest of the function pipeline.
All the tees
Many of the modules in fp-toolkit have support for tee functions to enable easy debugging, logging, and side effects.
Option.teeexecutes side effects on innerNomevaluesResult.teeexecutes side effects on innerOkvaluesResult.teeErrexecutes side effects on innerErrvaluesAsync.teeexecutes side effects on innerAsyncvalues
Contributing
Contributions to this library are welcomed and encouraged! Feel free to log new issues and open pull requests from forks. Clearly, issues with clear communication, code samples, and thorough explanation and reasoning will be actioned first. PRs need to have addressed all the adjacent issues: documentation, examples, and solid test coverage.
In other words, if you open an issue that just says, "there is a bug with this thing," don't expect that to get much traction. Or if you open a PR that is lacking any test coverage or has no documentation, again—don't expect that to get much traction.
Local development
Here are the important things for getting started developing on this project:
- We are using vite to build/bundle and vitest to test
npm run teststarts running tests in watch modenpm run test:coverageruns tests with coverage usingc8npm run buildbuilds the library with vitenpm run lintfmtlints and formats the code with biomenpm run lintfmt:fixlints, formats, and applies safe fixes to the code with biomenpm run docsuses TypeDoc to generate a static documentation website