Cookbook
Transform a collection
Section titled “Transform a collection”import { pipe } from '@stopcock/fp'import * as A from '@stopcock/fp/array'
type Product = { id: string; name: string; price: number; inStock: boolean }
const cards = pipe( products, A.filter((product: Product) => product.inStock), A.map((product: Product) => ({ id: product.id, label: product.name, price: product.price, })), A.take(20),)Compile a repeated hot path
Section titled “Compile a repeated hot path”import { compile } from '@stopcock/fp/compile'import { explain } from '@stopcock/fp/fusion/debug'import * as A from '@stopcock/fp/array'
const activeIds = compile( A.filter((user: User) => user.active), A.map((user: User) => user.id), A.take(100),)
activeIds(firstBatch)activeIds(secondBatch)console.log(explain(A.filter((user: User) => user.active), A.map((user: User) => user.id)))// 'sequential' unless @stopcock/fp-compiler fused this exact call at build timecompile is a marker: uncompiled it’s identical to pipe. @stopcock/fp-compiler
is what actually fuses a recognized call site into one loop at build time.
Use compilePure only when every callback is pure and the extra rewrite
freedom is valid for your code.
Read optional configuration
Section titled “Read optional configuration”import { pipe } from '@stopcock/fp'import * as O from '@stopcock/fp/option'
const port = pipe( O.fromNullable(process.env.PORT), O.map(Number), O.filter((value: number) => Number.isSafeInteger(value) && value > 0 && value < 65_536), O.getOrElse(() => 3000),)Decode fail-fast input
Section titled “Decode fail-fast input”import { pipe } from '@stopcock/fp'import * as G from '@stopcock/fp/guard'import * as R from '@stopcock/fp/result'
type DecodeError = | { readonly type: 'InvalidJson'; readonly cause: unknown } | { readonly type: 'ExpectedObject' }
const decodeBody = (text: string) => pipe( R.tryCatch( () => JSON.parse(text) as unknown, (cause): DecodeError => ({ type: 'InvalidJson', cause }), ), R.filterOrElse( G.isPlainObject, (): DecodeError => ({ type: 'ExpectedObject' }), ), )Accumulate independent field errors
Section titled “Accumulate independent field errors”import * as V from '@stopcock/fp/validation'
type FieldError = { readonly field: string readonly message: string}
const validateSignup = (email: string, password: string) => V.all([ V.fromPredicate( (value: string) => value.includes('@'), (): FieldError => ({ field: 'email', message: 'invalid email' }), )(email), V.fromPredicate( (value: string) => value.length >= 12, (): FieldError => ({ field: 'password', message: 'too short' }), )(password), ] as const)Use Result.flatMap for checks that depend on earlier values. Use Validation
only when every check can run independently.
Validate through Standard Schema
Section titled “Validate through Standard Schema”import * as Schema from '@stopcock/fp/schema'
const userId = Schema.fromPredicate( (value: unknown): value is string => typeof value === 'string' && value.length > 0, () => Schema.issue('Expected a user id', ['id']),)
const decoded = Schema.validateSync(userId)(input)The same consumer accepts a Standard Schema produced by another library.
Stream a large or infinite sequence
Section titled “Stream a large or infinite sequence”import { pipe } from '@stopcock/fp'import * as Iter from '@stopcock/fp/iter'
const firstTenEvenSquares = pipe( Iter.range(1, Infinity), Iter.filter((value) => value % 2 === 0), Iter.map((value) => value * value), Iter.take(10), Iter.toArray,)For asynchronous sources, use AsyncIter from @stopcock/async.
Update nested data immutably
Section titled “Update nested data immutably”import { pipe } from '@stopcock/fp'import * as Optic from '@stopcock/fp/optic'
const city = Optic.optic<User>() .prop('profile') .prop('address') .prop('city') .value
const moved = pipe( user, Optic.set(city, 'Manchester'),)Group and aggregate records
Section titled “Group and aggregate records”import * as A from '@stopcock/fp/array'
const revenueByRegion = A.groupMapReduce( (order: Order) => order.region, (order: Order) => order.total, (left: number, right: number) => left + right,)(orders)groupMapReduce, countBy, partitionMap, binary search, combinations, and
explicit mapInto/filterInto operations live in the array subpath.
Match tagged domain values
Section titled “Match tagged domain values”import * as Match from '@stopcock/fp/match'
type Payment = | { readonly type: 'Pending'; readonly id: string } | { readonly type: 'Paid'; readonly id: string; readonly receipt: string } | { readonly type: 'Failed'; readonly id: string; readonly reason: string }
const label = (payment: Payment) => Match.discriminant<'type', Payment, string>('type', { Pending: (value) => `Pending ${value.id}`, Paid: (value) => `Receipt ${value.receipt}`, Failed: (value) => `Failed: ${value.reason}`, })(payment)The handler object must cover every tag. Use the pattern module for structural patterns once simple tagged dispatch is no longer enough.