Claude Code for fp-ts: Functional Programming in TypeScript — Claude Skills 360 Blog
Blog / Backend / Claude Code for fp-ts: Functional Programming in TypeScript
Backend

Claude Code for fp-ts: Functional Programming in TypeScript

Published: April 7, 2027
Read time: 8 min read
By: Claude Skills 360

fp-ts brings Haskell-style functional programming to TypeScript — Option<A> models nullable values without null checks: some(value) or none. Either<E, A> models typed errors: right(value) for success, left(error) for failure. pipe(value, f1, f2, f3) composes left-to-right. flow(f1, f2, f3) creates a composed function. TaskEither<E, A> handles async operations with typed errors — TE.tryCatch(() => fetch(...), toError). IO<A> encapsulates synchronous side effects. Reader<R, A> injects dependencies. A.sequence(TE.ApplicativePar)([te1, te2]) runs TaskEithers in parallel and collects results. Do notation with bind reads like imperative code while staying pure. Claude Code generates fp-ts pipelines, TaskEither async chains, Option/Either conversions, and Reader dependency injection patterns.

CLAUDE.md for fp-ts

## fp-ts Stack
- Version: fp-ts >= 2.16
- Option: O.some(v) / O.none — O.fold(() => default, v => v)(opt)
- Either: E.right(v) / E.left(err) — E.chain, E.map, E.mapLeft
- pipe: pipe(value, E.chain(f), E.map(g)) — left-to-right composition
- TaskEither: TE.tryCatch(() => asyncFn(), toError) — TE.chain, TE.map
- Parallel: TE.sequenceArray or A.traverse(TE.ApplicativePar)
- Do: pipe(TE.Do, TE.bind("user", () => fetchUser()), TE.bind("orders", ({ user }) => fetchOrders(user.id)))
- Reader: R.asks((deps: Deps) => deps.db) — inject at program edge
- Decode: io-ts or zod for runtime validation → Either

Option — Nullable Values

import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import { pipe } from "fp-ts/function"

// Option replaces null/undefined
type User = { id: string; name: string; email: string; role?: string }

// fromNullable wraps nullable values
function findUser(users: User[], id: string): O.Option<User> {
  return O.fromNullable(users.find(u => u.id === id))
}

// Chain operations on present values
function getUserRole(users: User[], id: string): O.Option<string> {
  return pipe(
    findUser(users, id),
    O.chain(user => O.fromNullable(user.role)),
    O.map(role => role.toUpperCase()),
  )
}

// Fold to extract value with default
function displayRole(users: User[], id: string): string {
  return pipe(
    getUserRole(users, id),
    O.fold(
      () => "No role assigned",
      role => `Role: ${role}`,
    ),
  )
}

// getOrElse — simpler default fallback
function getUsername(user: O.Option<User>): string {
  return pipe(
    user,
    O.map(u => u.name),
    O.getOrElse(() => "Anonymous"),
  )
}

// sequence — all-or-nothing for arrays of Options
function getRequiredUsers(users: User[], ids: string[]): O.Option<User[]> {
  return pipe(
    ids,
    A.map(id => findUser(users, id)),
    O.sequenceArray,
    O.map(arr => Array.from(arr)),
  )
}

// fromPredicate — conditional wrapping
const activeUser = (user: User): O.Option<User> =>
  pipe(user, O.fromPredicate(u => u.role !== undefined))

Either — Typed Errors

import * as E from "fp-ts/Either"
import { pipe } from "fp-ts/function"

// Discriminated union for app errors
type AppError =
  | { type: "NOT_FOUND"; message: string }
  | { type: "VALIDATION"; field: string; message: string }
  | { type: "UNAUTHORIZED"; message: string }
  | { type: "DATABASE"; message: string; cause?: unknown }

const notFound = (msg: string): AppError => ({ type: "NOT_FOUND", message: msg })
const validation = (field: string, msg: string): AppError => ({ type: "VALIDATION", field, message: msg })
const unauthorized = (msg: string): AppError => ({ type: "UNAUTHORIZED", message: msg })

// Validate user input
function validateEmail(email: string): E.Either<AppError, string> {
  const trimmed = email.trim().toLowerCase()
  if (!trimmed) return E.left(validation("email", "Email is required"))
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
    return E.left(validation("email", "Invalid email format"))
  }
  return E.right(trimmed)
}

function validatePassword(password: string): E.Either<AppError, string> {
  if (password.length < 8) return E.left(validation("password", "Password must be at least 8 characters"))
  if (!/[A-Z]/.test(password)) return E.left(validation("password", "Password must contain uppercase letter"))
  return E.right(password)
}

// Combine validations
type RegisterInput = { email: string; password: string }
type ValidatedInput = { email: string; password: string }

function validateRegisterInput(input: RegisterInput): E.Either<AppError, ValidatedInput> {
  return pipe(
    E.Do,
    E.bind("email", () => validateEmail(input.email)),
    E.bind("password", () => validatePassword(input.password)),
  )
}

// Chain auth check
function checkPermission(user: { role: string }, required: string): E.Either<AppError, void> {
  return user.role === required || user.role === "admin"
    ? E.right(undefined)
    : E.left(unauthorized(`Requires ${required} role`))
}

// Map error types for API responses
function toHttpError(error: AppError): { status: number; message: string } {
  switch (error.type) {
    case "NOT_FOUND":    return { status: 404, message: error.message }
    case "VALIDATION":  return { status: 400, message: `${error.field}: ${error.message}` }
    case "UNAUTHORIZED": return { status: 401, message: error.message }
    case "DATABASE":    return { status: 500, message: "Internal server error" }
  }
}

TaskEither — Async Error Handling

import * as TE from "fp-ts/TaskEither"
import * as E from "fp-ts/Either"
import { pipe } from "fp-ts/function"

type User = { id: string; name: string; email: string }
type Order = { id: string; userId: string; totalCents: number; status: string }

// Wrap async operations in TaskEither
function fetchUser(userId: string): TE.TaskEither<AppError, User> {
  return TE.tryCatch(
    async () => {
      const res = await fetch(`/api/users/${userId}`)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      return res.json() as Promise<User>
    },
    (err): AppError => ({
      type: "DATABASE",
      message: "Failed to fetch user",
      cause: err,
    }),
  )
}

function fetchOrders(userId: string): TE.TaskEither<AppError, Order[]> {
  return TE.tryCatch(
    async () => {
      const res = await fetch(`/api/users/${userId}/orders`)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      return res.json() as Promise<Order[]>
    },
    (err): AppError => ({ type: "DATABASE", message: "Failed to fetch orders", cause: err }),
  )
}

function createOrder(data: { userId: string; totalCents: number }): TE.TaskEither<AppError, Order> {
  return TE.tryCatch(
    async () => {
      const res = await fetch("/api/orders", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      })
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      return res.json() as Promise<Order>
    },
    (err): AppError => ({ type: "DATABASE", message: "Failed to create order", cause: err }),
  )
}

// Do-notation — reads like imperative, stays pure
type UserOrderSummary = {
  user: User
  orders: Order[]
  totalSpent: number
}

function getUserOrderSummary(userId: string): TE.TaskEither<AppError, UserOrderSummary> {
  return pipe(
    TE.Do,
    TE.bind("user", () => fetchUser(userId)),
    TE.bind("orders", ({ user }) => fetchOrders(user.id)),
    TE.map(({ user, orders }) => ({
      user,
      orders,
      totalSpent: orders.reduce((sum, o) => sum + o.totalCents, 0),
    })),
  )
}

// Parallel execution with sequenceArray
function fetchMultipleUsers(userIds: string[]): TE.TaskEither<AppError, User[]> {
  return pipe(
    userIds,
    TE.traverseArray(userId => fetchUser(userId)),
    TE.map(arr => Array.from(arr)),
  )
}

// Conditional chain — only proceed if condition met
function placeOrderIfActive(
  userId: string,
  totalCents: number,
): TE.TaskEither<AppError, Order> {
  return pipe(
    fetchUser(userId),
    TE.chainW(user =>
      user.email.includes("@")
        ? createOrder({ userId: user.id, totalCents })
        : TE.left<AppError>({ type: "UNAUTHORIZED", message: "User account not verified" }),
    ),
  )
}

// Execute TaskEither — unwrap at program boundary
async function runGetSummary(userId: string) {
  const result = await getUserOrderSummary(userId)()
  if (E.isLeft(result)) {
    const httpError = toHttpError(result.left)
    return Response.json(httpError, { status: httpError.status })
  }
  return Response.json(result.right)
}

Reader — Dependency Injection

import * as R from "fp-ts/Reader"
import * as RTE from "fp-ts/ReaderTaskEither"
import { pipe } from "fp-ts/function"

// Dependencies interface
interface AppDeps {
  db: {
    users: { findById(id: string): Promise<User | null> }
    orders: { findByUserId(id: string): Promise<Order[]> }
  }
  logger: { info(msg: string, data?: unknown): void; error(msg: string, err: unknown): void }
  config: { maxOrdersPerUser: number }
}

// ReaderTaskEither — combine Reader + TaskEither
type RTE<A> = RTE.ReaderTaskEither<AppDeps, AppError, A>

function getUserRTE(userId: string): RTE<User> {
  return RTE.asksTaskEitherW(deps =>
    TE.tryCatch(
      async () => {
        const user = await deps.db.users.findById(userId)
        if (!user) throw new Error("Not found")
        return user
      },
      (err): AppError => ({ type: "NOT_FOUND", message: `User ${userId} not found` }),
    ),
  )
}

function getOrdersRTE(userId: string): RTE<Order[]> {
  return RTE.asksTaskEitherW(deps =>
    TE.tryCatch(
      () => deps.db.orders.findByUserId(userId),
      (err): AppError => ({ type: "DATABASE", message: "Orders fetch failed", cause: err }),
    ),
  )
}

// Compose with Reader
const getUserSummaryRTE = (userId: string): RTE<UserOrderSummary> =>
  pipe(
    RTE.Do,
    RTE.bind("user", () => getUserRTE(userId)),
    RTE.bind("orders", ({ user }) => getOrdersRTE(user.id)),
    RTE.map(({ user, orders }) => ({
      user,
      orders,
      totalSpent: orders.reduce((sum, o) => sum + o.totalCents, 0),
    })),
  )

// Inject dependencies at the edge
async function handleGetSummary(userId: string, deps: AppDeps) {
  const result = await getUserSummaryRTE(userId)(deps)()
  return E.fold(
    (err: AppError) => Response.json(toHttpError(err), { status: toHttpError(err).status }),
    (summary: UserOrderSummary) => Response.json(summary),
  )(result)
}

For the Effect alternative when a more ergonomic functional effect system with built-in dependency injection (Context.Tag), structured concurrency, resource management, and a richer standard library than fp-ts are preferred — Effect (formerly Effect-TS) has superseded fp-ts for many functional TypeScript projects with a more approachable API and better tooling, see the Effect guide. For the neverthrow alternative when a lighter, more pragmatic Result type without full functional programming primitives is preferred — neverthrow provides ok/err, ResultAsync, and .andThen() chaining without the full fp-ts ecosystem, making it easier to adopt incrementally, see the neverthrow guide. The Claude Skills 360 bundle includes fp-ts skill sets covering Option, Either, TaskEither, and Reader patterns. Start with the free tier to try functional TypeScript generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free