whatiskadudoing/fp-ts-skills/fp-compose

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Practical patterns for composing functions in TypeScript using pipe, flow, and functional design principles

Install

npx vskill@latest install whatiskadudoing/fp-ts-skills/fp-compose

SKILL.md

Function Composition - Building from Small Pieces

The core idea is simple:

typescript
pipe(data, fn1, fn2, fn3) === fn3(fn2(fn1(data)))

That's it. No category theory needed. Just chain functions together, left to right.


1. pipe() - Your New Best Friend

The Basic Pattern
typescript
import { pipe } from 'fp-ts/function'
// Instead of nested calls:
const result = formatOutput(calculateTotal(validateInput(parseData(rawInput))))
// Write it as a pipeline:
const result = pipe(
rawInput,
parseData,
validateInput,
calculateTotal,
formatOutput
)
Why This Matters

Before (nested calls):

typescript
// Read from inside out, right to left - confusing
const userName = capitalize(trim(getProperty('name')(user)))

After (pipe):

typescript
// Read top to bottom, left to right - natural
const userName = pipe(
user,
getProperty('name'),
trim,
capitalize
)
Real Example: Processing User Input
typescript
import { pipe } from 'fp-ts/function'
interface UserInput {
email: string
name: string
age: string
}
interface CleanUser {
email: string
name: string
age: number
}
// Small, focused functions
const trimEmail = (input: UserInput): UserInput => ({
...input,
email: input.email.trim().toLowerCase()
})
const trimName = (input: UserInput): UserInput => ({
...input,
name: input.name.trim()
})
const parseAge = (input: UserInput): CleanUser => ({
...input,
age: parseInt(input.age, 10) || 0
})
// Compose them
const cleanUserInput = (raw: UserInput): CleanUser =>
pipe(raw, trimEmail, trimName, parseAge)
// Use it
cleanUserInput({ email: ' ALICE@EMAIL.COM ', name: ' Alice ', age: '30' })
// { email: 'alice@email.com', name: 'Alice', age: 30 }

2. Building Reusable Utilities

The key is making small functions that do one thing well.

String Utilities
typescript
import { pipe, flow } from 'fp-ts/function'
// Basic building blocks
const trim = (s: string): string => s.trim()
const lowercase = (s: string): string => s.toLowerCase()
const uppercase = (s: string): string => s.toUpperCase()
const replace = (pattern: RegExp, replacement: string) =>
(s: string): string => s.replace(pattern, replacement)
const prefix = (pre: string) => (s: string): string => `${pre}${s}`
const suffix = (suf: string) => (s: string): string => `${s}${suf}`
// Combine them into useful utilities
const slugify = flow(
trim,
lowercase,
replace(/\s+/g, '-'),
replace(/[^a-z0-9-]/g, '')
)
const titleCase = flow(
trim,
lowercase,
replace(/\b\w/g, c => c.toUpperCase())
)
const kebabCase = flow(
trim,
replace(/([a-z])([A-Z])/g, '$1-$2'),
lowercase,
replace(/\s+/g, '-')
)
// Use them
slugify(' Hello World! ') // 'hello-world'
titleCase('hello world') // 'Hello World'
kebabCase('myVariableName') // 'my-variable-name'
Number Utilities
typescript
const clamp = (min: number, max: number) =>
(n: number): number => Math.max(min, Math.min(max, n))
const round = (decimals: number) =>
(n: number): number => Math.round(n * 10 ** decimals) / 10 ** decimals
const multiply = (factor: number) =>
(n: number): number => n * factor
const add = (amount: number) =>
(n: number): number => n + amount
// Combine for specific use cases
const toPercentage = flow(
multiply(100),
round(1),
suffix('%')
)
const formatPrice = flow(
round(2),
n => n.toFixed(2),
prefix('$')
)
const normalizeScore = flow(
clamp(0, 100),
round(0)
)
// Use them
toPercentage(0.8567) // '85.7%'
formatPrice(19.999) // '$20.00'
normalizeScore(105) // 100
Array Utilities
typescript
import * as A from 'fp-ts/Array'
import { pipe, flow } from 'fp-ts/function'
// Property accessors
const prop = <T, K extends keyof T>(key: K) =>
(obj: T): T[K] => obj[key]
// Predicates
const isNotNull = <T>(value: T | null | undefined): value is T =>
value != null
const hasLength = (min: number) =>
(arr: readonly unknown[]): boolean => arr.length >= min
// Combining filters and maps
interface Product {
id: string
name: string
price: number
inStock: boolean
}
const getInStockProductNames = flow(
A.filter((p: Product) => p.inStock),
A.map(prop('name'))
)
const getTotalValue = flow(
A.map((p: Product) => p.price),
A.reduce(0, (acc, price) => acc + price)
)
const getProductsSortedByPrice = flow(
A.sort<Product>((a, b) => a.price - b.price)
)

3. Data-Last for Flexibility

Why Argument Order Matters
typescript
// Data-first: Hard to compose
const map1 = <A, B>(arr: A[], fn: (a: A) => B): B[] => arr.map(fn)
// Can't easily create reusable functions:
const doubleAll = (arr: number[]) => map1(arr, n => n * 2) // Must wrap
// Data-last: Easy to compose
const map2 = <A, B>(fn: (a: A) => B) => (arr: A[]): B[] => arr.map(fn)
// Create reusable functions by partial application:
const doubleAll = map2((n: number) => n * 2)
// Works naturally in pipes:
pipe([1, 2, 3], doubleAll) // [2, 4, 6]
The Pattern
typescript
// General rule: configuration first, data last
// Good: Configuration -> Data
const filter = <A>(predicate: (a: A) => boolean) =>
(arr: A[]): A[] => arr.filter(predicate)
const map = <A, B>(fn: (a: A) => B) =>
(arr: A[]): B[] => arr.map(fn)
const formatWith = (formatter: Intl.NumberFormat) =>
(n: number): string => formatter.format(n)
// All work smoothly in pipes:
const processNumbers = flow(
filter((n: number) => n > 0),
map(n => n * 2),
formatWith(new Intl.NumberFormat('en-US'))
)
Converting Data-First APIs
typescript
// Many built-in methods are data-first (method on object)
// Wrap them to be data-last
// Date formatting
const formatDate = (options: Intl.DateTimeFormatOptions) =>
(locale: string) =>
(date: Date): string => date.toLocaleDateString(locale, options)
const formatShortDate = formatDate({ month: 'short', day: 'numeric' })('en-US')
pipe(new Date(), formatShortDate) // 'Jan 30'
// JSON operations
const parseJSON = <T>() =>
(str: string): T => JSON.parse(str)
const stringifyJSON = (indent?: number) =>
<T>(data: T): string => JSON.stringify(data, null, indent)
// Regular expressions
const match = (regex: RegExp) =>
(str: string): RegExpMatchArray | null => str.match(regex)
const test = (regex: RegExp) =>
(str: string): boolean => regex.test(str)
const split = (separator: string | RegExp) =>
(str: string): string[] => str.split(separator)

4. When Composition Helps (And When It Doesn't)

Good Uses for Composition

1. Multi-step data transformations:

typescript
// Processing API responses
const processApiResponse = flow(
extractData,
normalizeFields,
validateSchema,
transformForUI
)

2. Building specialized functions:

typescript
// Currency formatters from a general formatter
const formatCurrency = (locale: string, currency: string) =>
(amount: number): string =>
new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount)
const formatUSD = formatCurrency('en-US', 'USD')
const formatEUR = formatCurrency('de-DE', 'EUR')
const formatGBP = formatCurrency('en-GB', 'GBP')

3. Validation chains:

typescript
const validateEmail = flow(
trim,
lowercase,
(email: string) => email.includes('@') ? email : null
)
const validateUsername = flow(
trim,
(name: string) => name.length >= 3 ? name : null
)

4. Event handlers:

typescript
const handleFormSubmit = flow(
preventDefault,
extractFormData,
validateForm,
submitToAPI
)
When NOT to Compose

1. When a simple function is clearer:

typescript
// Overengineered:
const isAdult = flow(
prop<Person, 'age'>('age'),
gte(18)
)
// Just write it:
const isAdult = (person: Person): boolean => person.age >= 18

2. When you need multiple inputs:

typescript
// Awkward with composition:
const calculateDiscount = (price: number, discountPercent: number): number =>
pipe(
price,
multiply(1 - discountPercent / 100)
)
// The discountPercent is awkwardly captured
// Just use a regular function:
const calculateDiscount = (price: number, discountPercent: number): number =>
price * (1 - discountPercent / 100)

3. When debugging becomes hard:

typescript
// If you can't figure out what's happening:
const mysteryPipeline = flow(fn1, fn2, fn3, fn4, fn5, fn6, fn7)
// Break it up and name the stages:
const parse = flow(fn1, fn2)
const validate = flow(fn3, fn4)
const transform = flow(fn5, fn6, fn7)
// Or just use a regular function with intermediate variables:
const processData = (input: Input): Output => {
const parsed = parse(input)
const validated = validate(parsed)
const transformed = transform(validated)
return transformed
}

4. When the team doesn't know the pattern:

typescript
// If your team isn't familiar with FP:
const result = pipe(
data,
A.filter(isActive),
A.map(getName),
A.sort(ordString.compare)
)
// Consider the more familiar version:
const result = data
.filter(isActive)
.map(getName)
.sort((a, b) => a.localeCompare(b))
Decision Guide
SituationUse Composition?
Multi-step transformationYes
Building reusable utilitiesYes
Single operationNo
Multiple unrelated inputsNo
Complex branching logicMaybe not
Team unfamiliar with FPStart simple

5. Debugging Pipelines

The trace Function
typescript
// Simple debug helper
const trace = <A>(label: string) =>
(value: A): A => {
console.log(`${label}:`, value)
return value
}
// Use it in pipelines
const result = pipe(
input,
trace('input'),
step1,
trace('after step1'),
step2,
trace('after step2'),
step3,
trace('final')
)
Conditional Tracing
typescript
// Only trace in development
const traceIf = (enabled: boolean) =>
<A>(label: string) =>
(value: A): A => {
if (enabled) console.log(`${label}:`, value)
return value
}
const debug = traceIf(process.env.NODE_ENV === 'development')
pipe(
data,
debug('input'),
transform,
debug('output')
)
Structured Logging
typescript
// More sophisticated tracing
interface TraceOptions {
label: string
transform?: (value: unknown) => unknown
condition?: (value: unknown) => boolean
}
const traceWith = (options: TraceOptions) =>
<A>(value: A): A => {
if (!options.condition || options.condition(value)) {
const output = options.transform ? options.transform(value) : value
console.log(`[${options.label}]`, output)
}
return value
}
// Use it
pipe(
users,
traceWith({ label: 'users', transform: arr => `count: ${arr.length}` }),
A.filter(isActive),
traceWith({ label: 'active', transform: arr => `count: ${arr.length}` })
)
Breakpoint Debugging
typescript
// Insert a breakpoint
const breakpoint = <A>(value: A): A => {
debugger // Execution pauses here
return value
}
pipe(
data,
step1,
breakpoint, // Inspect value here
step2
)
Type Checking Mid-Pipeline
typescript
// Verify types are what you expect
const assertType = <Expected>() =>
<Actual extends Expected>(value: Actual): Actual => value
pipe(
data,
parseInput,
assertType<{ name: string; age: number }>(), // TypeScript error if wrong
formatOutput
)

Practical Patterns

Pattern 1: Data Processing Pipeline
typescript
import { pipe, flow } from 'fp-ts/function'
import * as A from 'fp-ts/Array'
import * as O from 'fp-ts/Option'
interface RawRecord {
id: string
timestamp: string
value: string
status: string
}
interface ProcessedRecord {
id: string
date: Date
value: number
isActive: boolean
}
// Individual processing steps
const parseTimestamp = (r: RawRecord) => ({
...r,
date: new Date(r.timestamp)
})

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 90 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
11
Repository forks
3
7-day trend score
12

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/whatiskadudoing/fp-ts-skills/fp-compose/badge)](https://verified-skill.com/skills/whatiskadudoing/fp-ts-skills/fp-compose)

Something wrong with this skill or its page? Tell us.