Socket
Socket
Sign inDemoInstall

fp-toolkit

Package Overview
Dependencies
0
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

    fp-toolkit

A toolbox of useful types and functions for getting $#!7 done using a functional-first approach. Made for TypeScript.


Version published
Weekly downloads
66
decreased by-50%
Maintainers
1
Created
Weekly downloads
 

Readme

Source

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

  1. Clear documentation & lots of examples
  2. Make the type system work for us, not the other way around
  3. No esoteric names or functional programming mumbo jumbo
  4. No category theory
  5. An API that's as simple as possible but no simpler
  6. Focus on function purity and data immutability
  7. 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:

  • uniq
  • sortBy
  • groupBy
  • union
  • take
  • skip
  • ...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:

  • range
  • destruct
  • make
  • first
  • ...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 of Async computations in series and collect the results
  • parallel—execute a collection of Async computations in parallel and collect the results
  • delay
  • bind—chain Async computations together, one after the other
  • tee—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 semantics
  • tryCatch
  • tee & 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 app
  • match—pattern match against the inner Result
  • bind—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:

  • ofNullish
  • toNullish
  • filter
  • refine
  • defaultValue
  • defaultWith
  • map & 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 & matchOrElse functions for exhaustive pattern matching
  • an automagical parse function
  • 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.

  • match
  • matchOrElse
  • ...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 type
  • deriveFrom—derive an equality or ordering comparer from one you already have
  • getComposite—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:

  • tee
  • teeAsync

Map

Work with Map data structures in a type-safe and immutable way. Offers helpful utilities like:

  • iter
  • reduce
  • filter
  • ...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:

  • map
  • bind
  • defaultWith
  • ...and more!

String

A bunch of useful functions for working with strings in a functional paradigm.

  • trim
  • capitalize
  • split
  • ...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.tee executes side effects on inner Nome values
  • Result.tee executes side effects on inner Ok values
  • Result.teeErr executes side effects on inner Err values
  • Async.tee executes side effects on inner Async values

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 test starts running tests in watch mode
  • npm run test:coverage runs tests with coverage using c8
  • npm run build builds the library with vite
  • npm run lintfmt lints and formats the code with biome
  • npm run lintfmt:fix lints, formats, and applies safe fixes to the code with biome
  • npm run docs uses TypeDoc to generate a static documentation website

Keywords

FAQs

Last updated on 26 Apr 2024

Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc