composable-functions

Types and functions to make composition easy and safe

README

composable-functions


A set of types and functions to make compositions easy and safe.

- 🛟 Type-Safe Compositions: Ensure robust type-safety during function composition, preventing incompatible functions from being combined and reducing runtime errors.
- 🔄 Promise and Error Handling: Focus on the happy-path of your functions, eliminating the need for verbose try/catch syntax.
- 🏝️ Isolated Business Logic: Split your code into composable functions, making your code easier to test and maintain.
- 🔒 End-to-End Type Safety: Achieve end-to-end type safety from the backend to the UI with serializable results, ensuring data integrity across your entire application.
- ⚡ Parallel and Sequential Compositions: Compose functions both in parallel - with all and collect - and sequentially - with pipe, branch, and sequence -, to manage complex data flows optimizing your code for performance and clarity.
- 🕵️‍♂️ Runtime Validation: Use withSchema or applySchema with your favorite parser for optional runtime validation of inputs and context, enforcing data integrity only when needed.
- 🚑 Resilient Error Handling: Leverage enhanced combinators like mapErrors and catchFailure to transform and handle errors more effectively.
- 📊 Traceable Compositions: Use the trace function to log and monitor your composable functions’ inputs and results, simplifying debugging and monitoring.

Quickstart


  1. ```
  2. npm i composable-functions
  3. ```

  1. ```tsx
  2. import { composable, pipe } from 'composable-functions'

  3. const faultyAdd = (a: number, b: number) => {
  4.   if (a === 1) throw new Error('a is 1')
  5.   return a + b
  6. }
  7. const show = (a: number) => String(a)
  8. const addAndShow = pipe(faultyAdd, show)

  9. const result = await addAndShow(2, 2)
  10. /*
  11. result = {
  12.   success: true,
  13.   data: "4",
  14.   errors: []
  15. }
  16. */
  17. const failedResult = await addAndShow(1, 2)
  18. /*
  19. failedResult = {
  20.   success: false,
  21.   errors: [<Error object>]
  22. }
  23. */
  24. ```

Composing type-safe functions

Let's say we want to compose two functions: add: (a: number, b:number) => number and toString: (a: number) => string. We also want the composition to preserve the types, so we can continue living in the happy world of type-safe coding. The result would be a function that adds and converts the result to string, something like addAndReturnString: (a: number, b: number) => string.

Performing this operation manually is straightforward

  1. ```typescript
  2. function addAndReturnString(a: number, b: number): string {
  3.   return toString(add(a, b))
  4. }
  5. ```

It would be neat if typescript could do the typing for us and provided a more generic mechanism to compose these functions. Something like what you find in libraries such as lodash

Using composables the code could be written as:

  1. ```typescript
  2. const addAndReturnString = pipe(add, toString)
  3. ```

We can also extend the same reasoning to functions that return promises in a transparent way. Imagine we have `add: (a: number, b:number) => Promise` and `toString: (a: number) => Promise`, the composition above would work in the same fashion, returning a function `addAndReturnString(a: number, b: number): Promise` that will wait for each promise in the chain before applying the next function.

This library also defines several operations besides the pipe to compose functions in arbitrary ways, giving a powerful tool for the developer to reason about the data flow without worrying about mistakenly connecting the wrong parameters or forgetting to unwrap some promise or handle some error along the way.

Adding runtime validation to the Composable

To ensure type safety at runtime, use the applySchema or withSchema functions to validate external inputs against defined schemas. These schemas can be specified with libraries such as Zod or ArkType.

Note that the resulting Composable will have unknown types for the parameters now that we rely on runtime validation.

  1. ```ts
  2. import { applySchema } from 'composable-functions'
  3. import { z } from 'zod'

  4. const addAndReturnWithRuntimeValidation = applySchema(
  5.   z.number(),
  6.   z.number(),
  7. )(addAndReturnString)

  8. // Or you could have defined schemas and implementation in one shot:
  9. const add = withSchema(z.number(), z.number())((a, b) => a + b)
  10. ```

For more information and examples, check the Handling external input guide.