Baike.dev
All toolsOnline toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
C

composable-functions

> 编程语言
Open source

Types and functions to make composition easy and safe

742 stars0 likes1 views
WebsiteGitHub

About

Types and functions to make composition easy and safe

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 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.

Go to API Reference

Table of contents

  • Quickstart
  • Composing type-safe functions
    • Adding runtime validation to the Composable
  • Creating primitive composables
  • Sequential composition
    • Using non-composables (mapping)
  • Parallel composition
  • Handling errors
    • Throwing
    • Catching
    • Mapping the errors
  • Unwrapping the result
  • Guides
    • Migrating from domain-functions
    • Handling external input
    • Defining constants for multiple functions (context)
    • Using custom parsers
  • Using Deno
  • Acknowledgements

Quickstart

bash
npm i composable-functions
typescript
import { composable, pipe } from 'composable-functions'

const faultyAdd = (a: number, b: number) => {
  if (a === 1) throw new Error('a is 1')
  return a + b
}
const show = (a: number) => String(a)
const addAndShow = pipe(faultyAdd, show)

const result = await addAndShow(2, 2)
/*
result = {
  success: true,
  data: "4",
  errors: []
}
*/
const failedResult = await addAndShow(1, 2)
/*
failedResult = {
  success: false,
  errors: []
}
*/

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

typescript
function addAndReturnString(a: number, b: number): string {
  return toString(add(a, b))
}

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:

typescript
const addAndReturnString = pipe(add, toString)

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 function to validate external inputs against defined schemas. These schemas can be specified with any library that implements the Standard Schema Spec, such as Zod, ArkType, Valibot, and etc.

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

typescript
import { applySchema } from 'composable-functions'
import { z } from 'zod'
import { type } from 'arktype'

const addAndReturnWithRuntimeValidation = applySchema(
  z.number(),
  type('number'),
)(addAndReturnString)

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

Creating primitive composables

A Composable is a function that returns a Promise> where T is any type you want to return. Values of the type Result will represent either a failure (which carries a list of errors) or a success, where the computation has returned a value within the type T.

We can create a Composable by wrapping a function with the composable method:

typescript
import { composable } from 'composable-functions'

const add = composable((a: number, b: number) => a + b)
//    ^? Composable number>

Or we can use combinators work with both plain functions and Composable to create other composables:

typescript
import { composable, pipe } from 'composable-functions'

const add = composable((a: number, b: number) => a + b)
//    ^? Composable number>
const toString = (a: unknown) => `${a}`

const addAndReturnString = pipe(add, toString)
//    ^? Composable string>

Sequential composition

We can compose the functions above using pipe to create addAndReturnString:

typescript
import { pipe } from 'composable-functions'

const addAndReturnString = pipe(add, toString)
//    ^? Composable string>

Note that trying to compose pipe flipping the arguments will not type-check:

typescript
import { pipe } from 'composable-functions'

const addAndReturnString = pipe(toString, add)
//    ^? Internal.FailToCompose

Since pipe will compose from left to right, the only string output from toString will not fit into the first argument of add which is a number. The error message comes in the form of an inferred FailToCompose type. This failure type is not callable, therefore it will break any attempts to call addAndReturnString.

Transforming the output (mapping)

Sometimes we want to use a simple function to transform the output of another function. Imagine you want to apply a plain old function to the result of add when it succeeds. The function map can be used for this:

typescript
import { map } from 'composable-functions'

const addAndReturnString = map(add, result => `${result}`)
//    ^? Composable string>

Parallel composition

There are also compositions where all functions are excuted in parallel, like Promise.all will execute several promises and wait for all of them. The all function is one way of composing in this fashion. Assuming we want to apply our add and multiply the two numbers returning a success only once both operations succeed:

typescript
import { all } from 'composable-functions'

const add = (a: number, b: number) => a + b
const mul = (a: number, b: number) => a * b
const addAndMul = all(add, mul)
//    ^? Composable [number, number]>

The result of the composition comes in a tuple in the same order as the functions were passed to all. Note that the input functions will also have to type-check and all the functions have to work from the same input.

If you want to work with records instead of tuples, you can use the collect function:

typescript
import { collect } from 'composable-functions'

const add = (a: number, b: number) => a + b
const mul = (a: number, b: number) => a * b
const addAndMul = collect({ add, mul })
//    ^? Composable { add: number, mul: number }>

Handling errors

Since a Composable always return a type Result that might be either a failure or a success, there are never exceptions to catch. Any exception inside a Composable will return as an object with the shape: { success: false, errors: Error[] }.

Two neat consequences is that we can handle errors using functions (no need for try/catch blocks) and handle multiple errors at once.

Throwing

You can throw anything derived from Error. Check this documentation on how to define your custom errors. The library will also wrap anything that does not extend Error, just to keep compatibility with code-bases that throw strings or objects.

typescript
import { composable } from 'composable-functions'

class NotFoundError extends Error {
  constructor(message) {
    super(message);
    this.name = 'NotFoundError';
  }
}

const getUser = composable((userId: string, users: Array) => {
//    ^? Composable) => string>
    const result = users.find(({id}) => id === userId)
    if(result == undefined) throw new NotFoundError(`userId ${userId} was not found`)
    return result
})

The library defines a few custom errors out of the box but these will be more important later on, when dealing with external input and schemas. See the errors module for more details.

Catching

You can catch an error in a Composable using catchFailure, which is similar to map but will run whenever the first composable fails:

typescript
import { catchFailure } from 'composable-functions'

// assuming we have the definitions from the previous example
const getOptionalUser = catchFailure(getUser, (errors, id) => {
  console.log(`Failed to fetch user with id ${id}`, errors)
  return null
})
//    ^? Composable string | null>

Mapping the errors

Sometimes we just need to transform the errors into something that would make more sense for the caller. Imagine you have our getUser defined above, but we want a custom error type for when the ID is invalid. You can map over the failures using mapErrors and a function with the type (errors: Error[]) => Error[].

typescript
import { mapErrors } from 'composable-functions'

class InvalidUserId extends Error {}
const getUserWithCustomError = mapErrors(getUser, (errors) =>
  errors.map((e) => e.message.includes('Invalid ID') ? new InvalidUserId() : e)
)

Unwrapping the result

Keep in mind the Result type will only have a data property when the composable succeeds. If you want to unwrap the result, you must check for the success property first.

typescript
const result = await getUser('123')
if (!result.success) return notFound()

return result.data
//            ^? User

TypeScript won't let you access the data property without checking for success first, so you can be sure that you are always handling the error case.

typescript
const result = await getUser('123')
// @ts-expect-error: Property 'data' does not exist on type 'Result'
return result.data

You can also use fromSuccess to unwrap the result of a composable that is expected to always succeed. Keep in mind that this function will throw an error if the composable fails so y

Issues· 0 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

TypeScript

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
Category编程语言
PricingOpen source

> Related tools

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言