smithery/neversight

fp-ts

Master the fp-ts library for typed functional programming in TypeScript, including Option, Either, Task, TaskEither, Reader, State, IO, Array, Record, pipe/flow composition, Do notation, optics (lenses/prisms), and integration with the Effect-TS ecosystem.

Installation

$ npx skills add smithery/neversight --skill fp-ts

Summary

  • Master the fp-ts library for typed functional programming in TypeScript, including Option, Either, Task, TaskEither, Reader, State, IO, Array, Record, pipe/flow composition, Do notation, optics (lenses/prisms), and integration with the Effect-TS ecosystem.
  • Use when working with fp-ts data types, composing functional pipelines, handling effects functionally, implementing monadic patterns, or using fp-ts utilities for type-safe functional code.

Also in this package

Other skills from smithery/neversight · top by installs.

npx skills add smithery/neversight

Browse all from smithery/neversight

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 29,552 B
  • docs SUMMARY.md 459 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

fp-ts Mastery

fp-ts is the most widely used library for typed functional programming in TypeScript, bringing abstractions from Haskell and Scala with strict type safety.

Installation and Setup

npm install fp-ts
// Import from specific modules
import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either';
import * as A from 'fp-ts/Array';
import * as TE from 'fp-ts/TaskEither';
import { pipe, flow } from 'fp-ts/function';

Core Concepts

pipe and flow

The foundation of fp-ts composition.

import { pipe, flow } from 'fp-ts/function';

// pipe - apply functions left-to-right on a value
const result = pipe(
  5,
  n => n * 2,    // 10
  n => n + 1,    // 11
  n => n.toString() // "11"
);

// flow - compose functions without initial value
const processNumber = flow(
  (n: number) => n * 2,
  n => n + 1,
  n => n.toString()
);

processNumber(5); // "11"

The HKT (Higher-Kinded Types) System

fp-ts uses a sophisticated type system for generic abstractions.

// Module augmentation for HKT
import { HKT, Kind, Kind2, URIS, URIS2 } from 'fp-ts/HKT';

// URIS identifies type constructors
const optionURI = 'Option';
type OptionURI = typeof optionURI;

// Use Kind to apply type constructor
type OptionKind<A> = Kind<OptionURI, A>;

Option Type

Represents optional values without null/undefined.

Construction

import * as O from 'fp-ts/Option';

// Create Option values
const some = O.some(42);           // Some(42)
const none = O.none;                // None
const fromNullable = O.fromNullable(maybeValue); // Some or None
const fromPredicate = O.fromPredicate((n: number) => n > 0)(5); // Some(5)

// Type: Option<number>

Core Operations

import { pipe } from 'fp-ts/function';
import * as O from 'fp-ts/Option';

// map - transform the value
pipe(
  O.some(5),
  O.map(n => n * 2)
); // Some(10)

// flatMap (chain) - sequencing operations that return Option
pipe(
  O.some(5),
  O.flatMap(n => n > 0 ? O.some(n * 2) : O.none)
); // Some(10)

// getOrElse - extract value with default
pipe(
  O.none,
  O.getOrElse(() => 0)
); // 0

// fold (match) - handle both cases
pipe(
  O.some(5),
  O.fold(
    () => 'No value',
    n => `Value: ${n}`
  )
); // "Value: 5"

// filter
pipe(
  O.some(5),
  O.filter(n => n > 3)
); // Some(5)

// alt - provide alternative Option
pipe(
  O.none,
  O.alt(() => O.some(42))
); // Some(42)

Advanced Patterns

// Traverse array of Options
import * as A from 'fp-ts/Array';

const options = [O.some(1), O.some(2), O.some(3)];

pipe(
  options,
  A.sequence(O.Applicative)
); // Some([1, 2, 3])

pipe(
  [O.some(1), O.none, O.some(3)],
  A.sequence(O.Applicative)
); // None

// Do notation for imperative-style sequencing
pipe(
  O.Do,
  O.bind('x', () => O.some(5)),
  O.bind('y', () => O.some(3)),
  O.map(({ x, y }) => x + y)
); // Some(8)

// exists and every
pipe(
  O.some(5),
  O.exists(n => n > 3)
); // true

// toNullable and toUndefined
pipe(
  O.some(5),
  O.toNullable
); // 5

pipe(
  O.none,
  O.toNullable
); // null

Common Use Cases

// Safe array access
const head = <A>(arr: readonly A[]): O.Option<A> =>
  pipe(arr, A.head);

// Safe property access
const getProp = <K extends string>(key: K) =>
  <T extends Record<K, unknown>>(obj: T): O.Option<T[K]> =>
    O.fromNullable(obj[key]);

// Safe parsing
const parseNumber = (s: string): O.Option<number> =>
  pipe(
    O.tryCatch(() => {
      const n = parseFloat(s);
      return isNaN(n) ? null : n;
    })
  );

// Chaining optional operations
type User = { name: string; address?: { city?: string } };

const getCity = (user: User): O.Option<string> =>
  pipe(
    user.address,
    O.fromNullable,
    O.flatMap(addr => O.fromNullable(addr.city))
  );

Either Type

Represents computations that can fail.

Construction

import * as E from 'fp-ts/Either';

// Create Either values
const right = E.right(42);               // Right(42)
const left = E.left('error');            // Left('error')
const fromPredicate = E.fromPredicate(
  (n: number) => n > 0,
  n => `${n} is not positive`
)(5); // Right(5)

// Type: Either<string, number>

Core Operations

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

// map - transform right value
pipe(
  E.right(5),
  E.map(n => n * 2)
); // Right(10)

// mapLeft - transform left value
pipe(
  E.left('error'),
  E.mapLeft(e => e.toUpperCase())
); // Left('ERROR')

// flatMap (chain) - sequence Either-returning operations
pipe(
  E.right(5),
  E.flatMap(n => n > 0 ? E.right(n * 2) : E.left('negative'))
); // Right(10)

// fold (match) - handle both cases
pipe(
  E.right(5),
  E.fold(
    error => `Error: ${error}`,
    value => `Success: ${value}`
  )
); // "Success: 5"

// getOrElse - extract value with default
pipe(
  E.left('error'),
  E.getOrElse(() => 0)
); // 0

// orElse - provide alternative Either
pipe(
  E.left('error'),
  E.orElse(() => E.right(42))
); // Right(42)

// swap - exchange left and right
pipe(
  E.right(5),
  E.swap
); // Left(5)

// bimap - map both sides
pipe(
  E.right<string, number>(5),
  E.bimap(
    e => e.toUpperCase(),
    n => n * 2
  )
); // Right(10)

Error Handling Patterns

// tryCatch - wrap throwing code
const safeParse = (json: string): E.Either<Error, unknown> =>
  E.tryCatch(
    () => JSON.parse(json),
    reason => new Error(`Parse error: ${reason}`)
  );

// Validation with Either
const validateEmail = (email: string): E.Either<string, string> =>
  email.includes('@') 
    ? E.right(email)
    : E.left('Invalid email');

const validateAge = (age: number): E.Either<string, number> =>
  age >= 18
    ? E.right(age)
    : E.left('Must be 18 or older');

// Chain validations
const validateUser = (email: string, age: number): E.Either<string, User> =>
  pipe(
    validateEmail(email),
    E.flatMap(validEmail =>
      pipe(
        validateAge(age),
        E.map(validAge => ({ email: validEmail, age: validAge }))
      )
    )
  );

// Do notation for cleaner syntax
const validateUserDo = (email: string, age: number): E.Either<string, User> =>
  pipe(
    E.Do,
    E.bind('email', () => validateEmail(email)),
    E.bind('age', () => validateAge(age))
  );

Combining Multiple Eithers

import * as A from 'fp-ts/Array';
import { sequenceT } from 'fp-ts/Apply';

// Sequence array of Eithers (fails on first error)
const eithers: E.Either<string, number>[] = [
  E.right(1),
  E.right(2),
  E.right(3)
];

pipe(
  eithers,
  A.sequence(E.Applicative)
); // Right([1, 2, 3])

// Parallel validation with sequenceT
pipe(
  sequenceT(E.Applicative)(
    validateEmail('[email protected]'),
    validateAge(25)
  )
); // Right(['[email protected]', 25])

// Convert to Option
pipe(
  E.right(5),
  E.toOption
); // Some(5)

pipe(
  E.left('error'),
  E.toOption
); // None

Task and TaskEither

Handle asynchronous operations.

Task

Lazy Promise (only executes when called).

import * as T from 'fp-ts/Task';
import { pipe } from 'fp-ts/function';

// Create Task
const delay = (ms: number): T.Task<void> =>
  () => new Promise(resolve => setTimeout(resolve, ms));

const fetchData = (): T.Task<Data> =>
  () => fetch('/api/data').then(r => r.json());

// map
pipe(
  fetchData(),
  T.map(data => data.items.length)
); // Task<number>

// flatMap (chain)
pipe(
  fetchData(),
  T.flatMap(data => 
    pipe(
      delay(1000),
      T.map(() => data)
    )
  )
); // Task<Data>

// Execute
const task = fetchData();
task().then(data => console.log(data));

TaskEither

Asynchronous operations that can fail.

import * as TE from 'fp-ts/TaskEither';
import { pipe } from 'fp-ts/function';

// Create TaskEither
const fetchUser = (id: number): TE.TaskEither<Error, User> =>
  TE.tryCatch(
    () => fetch(`/api/users/${id}`).then(r => {
      if (!r.ok) throw new Error('Not found');
      return r.json();
    }),
    reason => new Error(`Fetch failed: ${reason}`)
  );

// map - transform success value
pipe(
  fetchUser(1),
  TE.map(user => user.name)
); // TaskEither<Error, string>

// mapLeft - transform error
pipe(
  fetchUser(1),
  TE.mapLeft(error => ({ message: error.message, code: 500 }))
); // TaskEither<{message: string, code: number}, User>

// flatMap (chain) - sequence async operations
pipe(
  fetchUser(1),
  TE.flatMap(user => fetchPosts(user.id))
); // TaskEither<Error, Post[]>

// fold (match)
pipe(
  fetchUser(1),
  TE.fold(
    error => T.of(`Error: ${error.message}`),
    user => T.of(`User: ${user.name}`)
  )
)(); // Promise<string>

// getOrElse
pipe(
  fetchUser(1),
  TE.getOrElse(error => T.of(defaultUser))
)(); // Promise<User>

// orElse - provide alternative
pipe(
  fetchUser(1),
  TE.orElse(error => fetchUserFromCache(1))
); // TaskEither<Error, User>

Do Notation with TaskEither

const processUser = (id: number): TE.TaskEither<Error, Result> =>
  pipe(
    TE.Do,
    TE.bind('user', () => fetchUser(id)),
    TE.bind('posts', ({ user }) => fetchPosts(user.id)),
    TE.bind('comments', ({ posts }) => fetchComments(posts[0].id)),
    TE.map(({ user, posts, comments }) => ({
      user,
      postCount: posts.length,
      commentCount: comments.length
    }))
  );

Parallel Execution

import { sequenceT } from 'fp-ts/Apply';
import { sequenceArray } from 'fp-ts/Array';

// Execute in parallel with sequenceT
const fetchUserData = (id: number): TE.TaskEither<Error, UserData> =>
  pipe(
    sequenceT(TE.ApplicativePar)(
      fetchUser(id),
      fetchPosts(id),
      fetchComments(id)
    ),
    TE.map(([user, posts, comments]) => ({
      user,
      posts,
      comments
    }))
  );

// Execute array in parallel
const fetchUsers = (ids: number[]): TE.TaskEither<Error, User[]> =>
  pipe(
    ids.map(fetchUser),
    TE.sequenceArray  // or A.sequence(TE.ApplicativePar)
  );

// Execute array sequentially
const fetchUsersSeq = (ids: number[]): TE.TaskEither<Error, User[]> =>
  pipe(
    ids.map(fetchUser),
    A.sequence(TE.ApplicativeSeq)
  );

Array Operations

fp-ts provides powerful array utilities.

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

// Safe head and tail
pipe([1, 2, 3], A.head); // Some(1)
pipe([], A.head);        // None
pipe([1, 2, 3], A.tail); // Some([2, 3])

// filter and partition
pipe(
  [1, 2, 3, 4, 5],
  A.filter(n => n % 2 === 0)
); // [2, 4]

pipe(
  [1, 2, 3, 4, 5],
  A.partition(n => n % 2 === 0)
); // { left: [1, 3, 5], right: [2, 4] }

// filterMap - filter and transform in one pass
pipe(
  ['1', 'foo', '2', 'bar', '3'],
  A.filterMap(s => {
    const n = parseInt(s);
    return isNaN(n) ? O.none : O.some(n);
  })
); // [1, 2, 3]

// flatMap (chain)
pipe(
  [1, 2, 3],
  A.flatMap(n => [n, n * 2])
); // [1, 2, 2, 4, 3, 6]

// reduce
pipe(
  [1, 2, 3, 4, 5],
  A.reduce(0, (acc, n) => acc + n)
); // 15

// findFirst and findLast
pipe(
  [1, 2, 3, 4, 5],
  A.findFirst(n => n > 3)
); // Some(4)

// lookup - safe array access
pipe(
  [1, 2, 3],
  A.lookup(1)
); // Some(2)

// uniq - remove duplicates
pipe(
  [1, 2, 2, 3, 3, 3, 4],
  A.uniq(Eq.eqNumber)
); // [1, 2, 3, 4]

// sort
import * as Ord from 'fp-ts/Ord';

pipe(
  [3, 1, 4, 1, 5],
  A.sort(Ord.ordNumber)
); // [1, 1, 3, 4, 5]

// groupBy
pipe(
  ['foo', 'bar', 'baz', 'qux'],
  A.groupBy(s => s[0])
); // { f: ['foo'], b: ['bar', 'baz'], q: ['qux'] }

// zip
pipe(
  A.zip([1, 2, 3], ['a', 'b', 'c'])
); // [[1, 'a'], [2, 'b'], [3, 'c']]

// chunksOf
pipe(
  [1, 2, 3, 4, 5, 6, 7],
  A.chunksOf(3)
); // [[1, 2, 3], [4, 5, 6], [7]]

Traversing with Effects

// Traverse with Option
const parseNumbers = (strs: string[]): O.Option<number[]> =>
  pipe(
    strs,
    A.traverse(O.Applicative)(s => {
      const n = parseInt(s);
      return isNaN(n) ? O.none : O.some(n);
    })
  );

parseNumbers(['1', '2', '3']); // Some([1, 2, 3])
parseNumbers(['1', 'foo', '3']); // None

// Traverse with Either
const validateAll = (
  users: UnvalidatedUser[]
): E.Either<string, User[]> =>
  pipe(
    users,
    A.traverse(E.Applicative)(validateUser)
  );

// Traverse with TaskEither
const fetchAllUsers = (ids: number[]): TE.TaskEither<Error, User[]> =>
  pipe(
    ids,
    A.traverse(TE.ApplicativePar)(fetchUser)
  );

Record Operations

Work with objects functionally.

import * as R from 'fp-ts/Record';
import { pipe } from 'fp-ts/function';

// map - transform all values
pipe(
  { a: 1, b: 2, c: 3 },
  R.map(n => n * 2)
); // { a: 2, b: 4, c: 6 }

// filter
pipe(
  { a: 1, b: 2, c: 3 },
  R.filter(n => n > 1)
); // { b: 2, c: 3 }

// filterMap
pipe(
  { a: '1', b: 'foo', c: '2' },
  R.filterMap(s => {
    const n = parseInt(s);
    return isNaN(n) ? O.none : O.some(n);
  })
); // { a: 1, c: 2 }

// lookup - safe property access
pipe(
  { a: 1, b: 2 },
  R.lookup('a')
); // Some(1)

// has - check key existence
pipe(
  { a: 1, b: 2 },
  R.has('c')
); // false

// keys and values
R.keys({ a: 1, b: 2, c: 3 }); // ['a', 'b', 'c']
R.collect((k, v) => [k, v])({ a: 1, b: 2 }); // [['a', 1], ['b', 2]]

// fromFoldable - create from iterable
import * as A from 'fp-ts/Array';

pipe(
  [['a', 1], ['b', 2], ['c', 3]],
  R.fromFoldable(
    { concat: (x, y) => y }, // last value wins
    A.Foldable
  )
); // { a: 1, b: 2, c: 3 }

// traverse with effects
const validateRecord = (
  record: Record<string, string>
): E.Either<string, Record<string, number>> =>
  pipe(
    record,
    R.traverse(E.Applicative)(s => {
      const n = parseInt(s);
      return isNaN(n) 
        ? E.left(`Invalid number: ${s}`)
        : E.right(n);
    })
  );

Reader Monad

Thread configuration/dependencies through computations.

import * as R from 'fp-ts/Reader';
import { pipe } from 'fp-ts/function';

type Config = {
  apiUrl: string;
  timeout: number;
};

// Create Reader
const getApiUrl: R.Reader<Config, string> = 
  config => config.apiUrl;

const getTimeout: R.Reader<Config, number> =
  config => config.timeout;

// map
const getFullUrl = (path: string): R.Reader<Config, string> =>
  pipe(
    getApiUrl,
    R.map(url => `${url}${path}`)
  );

// flatMap (chain)
const fetchWithTimeout = (path: string): R.Reader<Config, Promise<Response>> =>
  pipe(
    R.Do,
    R.bind('url', () => getFullUrl(path)),
    R.bind('timeout', () => getTimeout),
    R.map(({ url, timeout }) =>
      fetch(url, { signal: AbortSignal.timeout(timeout) })
    )
  );

// ask - get the environment
const logConfig: R.Reader<Config, void> =
  pipe(
    R.ask<Config>(),
    R.map(config => console.log(config))
  );

// local - modify environment locally
const withDifferentUrl = <A>(
  reader: R.Reader<Config, A>
): R.Reader<Config, A> =>
  pipe(
    reader,
    R.local((config: Config) => ({
      ...config,
      apiUrl: 'https://api-v2.example.com'
    }))
  );

// Execute Reader
const config: Config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
};

const result = fetchWithTimeout('/users')(config);

ReaderTaskEither

Combine Reader, Task, and Either for dependency injection with async error handling.

import * as RTE from 'fp-ts/ReaderTaskEither';
import { pipe } from 'fp-ts/function';

type Deps = {
  db: Database;
  logger: Logger;
  config: Config;
};

// Create RTE
const getUser = (id: number): RTE.ReaderTaskEither<Deps, Error, User> =>
  pipe(
    RTE.ask<Deps>(),
    RTE.flatMap(({ db, logger }) =>
      RTE.tryCatch(
        async () => {
          logger.info(`Fetching user ${id}`);
          return db.users.findById(id);
        },
        reason => new Error(`Failed to fetch user: ${reason}`)
      )
    )
  );

// Compose operations
const getUserWithPosts = (
  id: number
): RTE.ReaderTaskEither<Deps, Error, UserWithPosts> =>
  pipe(
    RTE.Do,
    RTE.bind('user', () => getUser(id)),
    RTE.bind('posts', ({ user }) => getPosts(user.id)),
    RTE.map(({ user, posts }) => ({ ...user, posts }))
  );

// Execute
const deps: Deps = {
  db: createDatabase(),
  logger: createLogger(),
  config: loadConfig()
};

getUserWithPosts(1)(deps)()
  .then(E.fold(
    error => console.error(error),
    user => console.log(user)
  ));

// local - modify dependencies
const withTestDb = <A>(
  rte: RTE.ReaderTaskEither<Deps, Error, A>
): RTE.ReaderTaskEither<Deps, Error, A> =>
  pipe(
    rte,
    RTE.local((deps: Deps) => ({
      ...deps,
      db: createTestDatabase()
    }))
  );

State Monad

Thread state through computations.

import * as S from 'fp-ts/State';
import { pipe } from 'fp-ts/function';

type Counter = { count: number };

// Create State
const increment: S.State<Counter, number> =
  state => [state.count + 1, { count: state.count + 1 }];

const decrement: S.State<Counter, number> =
  state => [state.count - 1, { count: state.count - 1 }];

// get and put
const getCount: S.State<Counter, number> =
  state => [state.count, state];

const setCount = (count: number): S.State<Counter, void> =>
  _state => [undefined, { count }];

// modify
const multiplyCount = (factor: number): S.State<Counter, void> =>
  S.modify((state: Counter) => ({ count: state.count * factor }));

// Compose operations
const complexOperation: S.State<Counter, string> =
  pipe(
    S.Do,
    S.bind('initial', () => getCount),
    S.bind('after1', () => increment),
    S.bind('after2', () => increment),
    S.bind('multiplied', () => {
      multiplyCount(2);
      return getCount;
    }),
    S.map(({ initial, after1, after2, multiplied }) =>
      `${initial} -> ${after1} -> ${after2} -> ${multiplied}`
    )
  );

// Execute State
const initialState: Counter = { count: 0 };
const [result, finalState] = complexOperation(initialState);

IO Monad

Encapsulate side effects.

import * as IO from 'fp-ts/IO';
import { pipe } from 'fp-ts/function';

// Create IO
const log = (message: string): IO.IO<void> =>
  () => console.log(message);

const random: IO.IO<number> =
  () => Math.random();

const now: IO.IO<Date> =
  () => new Date();

// map
pipe(
  random,
  IO.map(n => n * 100)
); // IO<number>

// flatMap (chain)
pipe(
  random,
  IO.flatMap(n => log(`Random: ${n}`))
); // IO<void>

// Do notation
const program: IO.IO<string> =
  pipe(
    IO.Do,
    IO.bind('time', () => now),
    IO.bind('rand', () => random),
    IO.chainFirst(({ time }) => log(`Time: ${time}`)),
    IO.map(({ time, rand }) => `${time}: ${rand}`)
  );

// Execute at program boundary
program();

Optics (Lenses, Prisms, Traversals)

Access and modify nested data structures immutably.

import { pipe } from 'fp-ts/function';
import * as O from 'fp-ts/Option';
import { Lens, Optional, Prism } from 'monocle-ts';

type Address = {
  street: string;
  city: string;
  zipCode: string;
};

type Person = {
  name: string;
  age: number;
  address: Address;
};

// Lens - focus on a field
const addressLens = Lens.fromProp<Person>()('address');
const cityLens = Lens.fromProp<Address>()('city');

// Compose lenses
const personCityLens = pipe(addressLens, Lens.compose(cityLens));

const person: Person = {
  name: 'John',
  age: 30,
  address: { street: '123 Main', city: 'NYC', zipCode: '10001' }
};

// Get
personCityLens.get(person); // 'NYC'

// Set
const updated = personCityLens.set('LA')(person);
// { ..., address: { ..., city: 'LA' } }

// Modify
const capitalized = personCityLens.modify(city => city.toUpperCase())(person);

// Optional - for nullable fields
type User = {
  name: string;
  email?: string;
};

const emailOptional = Optional.fromNullableProp<User>()('email');

const user: User = { name: 'John', email: '[email protected]' };

emailOptional.getOption(user); // Some('[email protected]')
emailOptional.set('[email protected]')(user);

// Prism - for sum types
type Shape =
  | { type: 'circle'; radius: number }
  | { type: 'rectangle'; width: number; height: number };

const circlePrism = Prism.fromPredicate((s: Shape): s is Extract<Shape, { type: 'circle' }> =>
  s.type === 'circle'
);

const shape: Shape = { type: 'circle', radius: 5 };

circlePrism.getOption(shape); // Some({ type: 'circle', radius: 5 })
circlePrism.modify(c => ({ ...c, radius: c.radius * 2 }))(shape);

// Traversal - for collections
import { Traversal } from 'monocle-ts';
import * as A from 'fp-ts/Array';

const arrayTraversal = <A>() => Traversal.fromTraversable(A.Traversable)<A>();

const numbers = [1, 2, 3, 4, 5];

pipe(
  numbers,
  arrayTraversal<number>().modify(n => n * 2)
); // [2, 4, 6, 8, 10]

Eq, Ord, and Semigroup

Type classes for comparison and combination.

import * as Eq from 'fp-ts/Eq';
import * as Ord from 'fp-ts/Ord';
import * as S from 'fp-ts/Semigroup';
import { pipe } from 'fp-ts/function';

// Eq - equality checking
const eqPerson = Eq.struct<Person>({
  name: Eq.eqString,
  age: Eq.eqNumber,
  address: Eq.struct({
    street: Eq.eqString,
    city: Eq.eqString,
    zipCode: Eq.eqString
  })
});

eqPerson.equals(person1, person2); // boolean

// Ord - ordering
const ordPerson = pipe(
  Ord.ordNumber,
  Ord.contramap((p: Person) => p.age)
);

const people = [person1, person2, person3];
pipe(people, A.sort(ordPerson));

// Semigroup - combining values
const semigroupSum = S.semigroupSum;
S.concatAll(semigroupSum)(0)([1, 2, 3, 4]); // 10

const semigroupProduct = S.semigroupProduct;
S.concatAll(semigroupProduct)(1)([2, 3, 4]); // 24

// Custom semigroup
type User = { name: string; age: number };

const userSemigroup: S.Semigroup<User> = {
  concat: (x, y) => ({
    name: `${x.name} & ${y.name}`,
    age: Math.max(x.age, y.age)
  })
};

// Monoid - Semigroup with identity
import * as M from 'fp-ts/Monoid';

const monoidString = M.monoidString;
M.concatAll(monoidString)(['hello', ' ', 'world']); // 'hello world'

Practical Patterns

API Request Pipeline

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

type ApiError = 
  | { type: 'NetworkError'; message: string }
  | { type: 'ParseError'; message: string }
  | { type: 'ValidationError'; errors: string[] };

const request = <A>(
  url: string,
  options?: RequestInit
): TE.TaskEither<ApiError, A> =>
  pipe(
    TE.tryCatch(
      () => fetch(url, options),
      reason => ({ 
        type: 'NetworkError' as const, 
        message: String(reason) 
      })
    ),
    TE.flatMap(response =>
      TE.tryCatch(
        () => response.json(),
        reason => ({ 
          type: 'ParseError' as const, 
          message: String(reason) 
        })
      )
    )
  );

const validateUser = (data: unknown): E.Either<ApiError, User> => {
  // Validation logic
  if (isValidUser(data)) {
    return E.right(data as User);
  }
  return E.left({ 
    type: 'ValidationError', 
    errors: ['Invalid user data'] 
  });
};

const fetchUser = (id: number): TE.TaskEither<ApiError, User> =>
  pipe(
    request<unknown>(`/api/users/${id}`),
    TE.flatMapEither(validateUser)
  );

Form Validation

import * as E from 'fp-ts/Either';
import * as A from 'fp-ts/Array';
import { pipe } from 'fp-ts/function';
import { sequenceT } from 'fp-ts/Apply';

type ValidationError = {
  field: string;
  message: string;
};

type Validation<A> = E.Either<ValidationError[], A>;

const validateRequired = (field: string) => (value: string): Validation<string> =>
  value.length > 0
    ? E.right(value)
    : E.left([{ field, message: 'Required' }]);

const validateEmail = (value: string): Validation<string> =>
  value.includes('@')
    ? E.right(value)
    : E.left([{ field: 'email', message: 'Invalid email' }]);

const validateAge = (value: number): Validation<number> =>
  value >= 18
    ? E.right(value)
    : E.left([{ field: 'age', message: 'Must be 18+' }]);

// Applicative validation (accumulates all errors)
import * as Ap from 'fp-ts/Apply';

const getValidationApplicative = <E>(): Ap.Applicative2C<'Either', E[]> => ({
  ...E.Applicative,
  ap: (fab, fa) =>
    pipe(
      fab,
      E.flatMap(f =>
        pipe(
          fa,
          E.map(f),
          E.mapLeft(e1 =>
            pipe(
              fab,
              E.mapLeft(e2 => [...e2, ...e1]),
              E.getLeft,
              O.getOrElse((): E[] => e1)
            )
          )
        )
      )
    )
});

const validateForm = (
  email: string,
  age: number
): Validation<{ email: string; age: number }> =>
  pipe(
    sequenceT(getValidationApplicative<ValidationError>())(
      pipe(email, validateRequired('email'), E.flatMap(validateEmail)),
      validateAge(age)
    ),
    E.map(([email, age]) => ({ email, age }))
  );

Dependency Injection

import * as RTE from 'fp-ts/ReaderTaskEither';
import { pipe } from 'fp-ts/function';

type Services = {
  userRepo: UserRepository;
  emailService: EmailService;
  logger: Logger;
};

class UserService {
  getUser(id: number): RTE.ReaderTaskEither<Services, Error, User> {
    return pipe(
      RTE.ask<Services>(),
      RTE.flatMap(({ userRepo, logger }) =>
        RTE.tryCatch(
          async () => {
            logger.info(`Fetching user ${id}`);
            return userRepo.findById(id);
          },
          e => new Error(`Failed: ${e}`)
        )
      )
    );
  }

  createUser(data: UserData): RTE.ReaderTaskEither<Services, Error, User> {
    return pipe(
      RTE.Do,
      RTE.bind('services', () => RTE.ask<Services>()),
      RTE.bind('user', ({ services }) =>
        RTE.tryCatch(
          () => services.userRepo.create(data),
          e => new Error(`Failed: ${e}`)
        )
      ),
      RTE.chainFirst(({ services, user }) =>
        RTE.fromTask(() => 
          services.emailService.sendWelcome(user.email)
        )
      ),
      RTE.map(({ user }) => user)
    );
  }
}

// Usage
const services: Services = {
  userRepo: new UserRepository(),
  emailService: new EmailService(),
  logger: new Logger()
};

const userService = new UserService();

userService.createUser(userData)(services)()
  .then(E.fold(
    error => console.error(error),
    user => console.log('Created:', user)
  ));

Best Practices

  1. Use pipe for data flow: Always use pipe for left-to-right data transformation
  2. Leverage Do notation: Use Do notation for imperative-style sequencing when clearer
  3. Choose appropriate effects: Use TaskEither for async+errors, Reader for DI, IO for side effects
  4. Prefer traverse over manual loops: Use traverse and sequence for effectful operations
  5. Type your errors explicitly: Use discriminated unions for error types
  6. Use Applicative for parallel: Use ApplicativePar for parallel execution
  7. Compose with flow: Use flow to create reusable function compositions
  8. Avoid nesting: Flatten nested structures with flatMap
  9. Use type classes: Leverage Eq, Ord, Semigroup, Monoid for generic operations
  10. Test with property-based testing: fp-ts types work great with fast-check

Common Patterns

Error Recovery

const fetchWithRetry = <A>(
  fetch: TE.TaskEither<Error, A>,
  maxRetries: number
): TE.TaskEither<Error, A> => {
  const retry = (n: number): TE.TaskEither<Error, A> =>
    pipe(
      fetch,
      TE.orElse(error =>
        n > 0
          ? pipe(
              T.delay(1000)(T.of(undefined)),
              TE.fromTask,
              TE.flatMap(() => retry(n - 1))
            )
          : TE.left(error)
      )
    );
  
  return retry(maxRetries);
};

Caching

const cached = <A>(
  fetch: TE.TaskEither<Error, A>
): TE.TaskEither<Error, A> => {
  let cache: O.Option<A> = O.none;
  
  return pipe(
    cache,
    O.fold(
      () =>
        pipe(
          fetch,
          TE.map(value => {
            cache = O.some(value);
            return value;
          })
        ),
      value => TE.right(value)
    )
  );
};

Resource Management

const bracket = <R, E, A>(
  acquire: TE.TaskEither<E, R>,
  use: (r: R) => TE.TaskEither<E, A>,
  release: (r: R) => TE.TaskEither<E, void>
): TE.TaskEither<E, A> =>
  pipe(
    acquire,
    TE.flatMap(resource =>
      pipe(
        use(resource),
        TE.chainFirst(() => release(resource))
      )
    )
  );

Integration with Effect-TS

fp-ts is evolving as part of the Effect-TS ecosystem, which provides even richer effect and functional abstractions. Consider Effect for new projects requiring advanced features like fiber-based concurrency, structured concurrency, resource management, and more sophisticated effect systems.

import { Effect } from 'effect';

// Effect provides more powerful abstractions
const program = Effect.gen(function* (_) {
  const user = yield* _(fetchUser(1));
  const posts = yield* _(fetchPosts(user.id));
  return { user, posts };
});