Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    neversight

    fp-ts

    neversight/fp-ts
    Coding
    2
    1 installs

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

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

    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('test@example.com'),
        validateAge(25)
      )
    ); // Right(['test@example.com', 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: 'john@example.com' };
    
    emailOptional.getOption(user); // Some('john@example.com')
    emailOptional.set('new@example.com')(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 };
    });
    
    Recommended Servers
    Google Tasks
    Google Tasks
    Repository
    neversight/skills_feed