{"id":"03e6e53e-9e4f-4cde-a2d6-a4efa73153f7","shortId":"bAzzcr","kind":"skill","title":"fp-refactor","tagline":"Comprehensive guide for refactoring imperative TypeScript code to fp-ts functional patterns","description":"# Refactoring Imperative Code to fp-ts\n\nThis skill provides comprehensive patterns and strategies for migrating existing imperative TypeScript code to fp-ts functional programming patterns.\n\n## When to Use\n- You are refactoring an existing imperative TypeScript codebase toward fp-ts patterns.\n- The task involves converting `try/catch`, null checks, callbacks, DI, or loops into functional equivalents.\n- You need migration guidance and tradeoffs, not just isolated fp-ts examples.\n\n## Table of Contents\n\n1. [Converting try-catch to Either/TaskEither](#1-converting-try-catch-to-eithertaskeither)\n2. [Converting null checks to Option](#2-converting-null-checks-to-option)\n3. [Converting callbacks to Task](#3-converting-callbacks-to-task)\n4. [Converting class-based DI to Reader](#4-converting-class-based-di-to-reader)\n5. [Converting imperative loops to functional operations](#5-converting-imperative-loops-to-functional-operations)\n6. [Migrating Promise chains to TaskEither](#6-migrating-promise-chains-to-taskeither)\n7. [Common Pitfalls](#7-common-pitfalls)\n8. [Gradual Adoption Strategies](#8-gradual-adoption-strategies)\n9. [When NOT to Refactor](#9-when-not-to-refactor)\n\n---\n\n## 1. Converting try-catch to Either/TaskEither\n\n### The Problem with try-catch\n\nTraditional try-catch blocks have several issues:\n- Error handling is implicit and easy to forget\n- The type system doesn't track which functions can throw\n- Control flow is non-linear and harder to reason about\n- Composing multiple fallible operations is verbose\n\n### Pattern: Synchronous try-catch to Either\n\n#### Before (Imperative)\n\n```typescript\nfunction parseJSON(input: string): unknown {\n  try {\n    return JSON.parse(input);\n  } catch (error) {\n    throw new Error(`Invalid JSON: ${error}`);\n  }\n}\n\nfunction validateUser(data: unknown): User {\n  try {\n    if (!data || typeof data !== 'object') {\n      throw new Error('Data must be an object');\n    }\n    const obj = data as Record<string, unknown>;\n    if (typeof obj.name !== 'string') {\n      throw new Error('Name is required');\n    }\n    if (typeof obj.age !== 'number') {\n      throw new Error('Age must be a number');\n    }\n    return { name: obj.name, age: obj.age };\n  } catch (error) {\n    throw error;\n  }\n}\n\n// Usage with nested try-catch\nfunction processUserInput(input: string): User | null {\n  try {\n    const data = parseJSON(input);\n    const user = validateUser(data);\n    return user;\n  } catch (error) {\n    console.error('Failed to process user:', error);\n    return null;\n  }\n}\n```\n\n#### After (fp-ts Either)\n\n```typescript\nimport * as E from 'fp-ts/Either';\nimport * as J from 'fp-ts/Json';\nimport { pipe } from 'fp-ts/function';\n\ninterface User {\n  name: string;\n  age: number;\n}\n\n// Use Json.parse which returns Either<Error, Json>\nconst parseJSON = (input: string): E.Either<Error, unknown> =>\n  pipe(\n    J.parse(input),\n    E.mapLeft((e) => new Error(`Invalid JSON: ${e}`))\n  );\n\n// Validation returns Either, making errors explicit in types\nconst validateUser = (data: unknown): E.Either<Error, User> => {\n  if (!data || typeof data !== 'object') {\n    return E.left(new Error('Data must be an object'));\n  }\n  const obj = data as Record<string, unknown>;\n  if (typeof obj.name !== 'string') {\n    return E.left(new Error('Name is required'));\n  }\n  if (typeof obj.age !== 'number') {\n    return E.left(new Error('Age must be a number'));\n  }\n  return E.right({ name: obj.name, age: obj.age });\n};\n\n// Compose with pipe and flatMap - errors propagate automatically\nconst processUserInput = (input: string): E.Either<Error, User> =>\n  pipe(\n    parseJSON(input),\n    E.flatMap(validateUser)\n  );\n\n// Handle both cases explicitly\npipe(\n  processUserInput('{\"name\": \"Alice\", \"age\": 30}'),\n  E.match(\n    (error) => console.error('Failed to process user:', error.message),\n    (user) => console.log('User:', user)\n  )\n);\n```\n\n### Step-by-Step Refactoring Guide\n\n1. **Identify the error type**: Determine what errors can occur and create appropriate error types\n2. **Change return type**: From `T` to `Either<E, T>` where `E` is your error type\n3. **Replace throw statements**: Convert `throw new Error(...)` to `E.left(new Error(...))`\n4. **Replace return statements**: Convert `return value` to `E.right(value)`\n5. **Remove try-catch blocks**: They're no longer needed\n6. **Update callers**: Use `pipe` with `E.flatMap` to chain operations\n\n### Pattern: Async try-catch to TaskEither\n\n#### Before (Imperative)\n\n```typescript\nasync function fetchUser(id: string): Promise<User> {\n  try {\n    const response = await fetch(`/api/users/${id}`);\n    if (!response.ok) {\n      throw new Error(`HTTP error: ${response.status}`);\n    }\n    const data = await response.json();\n    return validateUser(data);\n  } catch (error) {\n    throw new Error(`Failed to fetch user: ${error}`);\n  }\n}\n\nasync function fetchUserPosts(userId: string): Promise<Post[]> {\n  try {\n    const response = await fetch(`/api/users/${userId}/posts`);\n    if (!response.ok) {\n      throw new Error(`HTTP error: ${response.status}`);\n    }\n    return await response.json();\n  } catch (error) {\n    throw new Error(`Failed to fetch posts: ${error}`);\n  }\n}\n\n// Complex orchestration with try-catch\nasync function getUserWithPosts(id: string): Promise<{ user: User; posts: Post[] } | null> {\n  try {\n    const user = await fetchUser(id);\n    const posts = await fetchUserPosts(id);\n    return { user, posts };\n  } catch (error) {\n    console.error(error);\n    return null;\n  }\n}\n```\n\n#### After (fp-ts TaskEither)\n\n```typescript\nimport * as TE from 'fp-ts/TaskEither';\nimport * as E from 'fp-ts/Either';\nimport { pipe } from 'fp-ts/function';\n\n// Wrap fetch in TaskEither\nconst fetchUser = (id: string): TE.TaskEither<Error, User> =>\n  pipe(\n    TE.tryCatch(\n      () => fetch(`/api/users/${id}`),\n      (reason) => new Error(`Network error: ${reason}`)\n    ),\n    TE.flatMap((response) =>\n      response.ok\n        ? TE.right(response)\n        : TE.left(new Error(`HTTP error: ${response.status}`))\n    ),\n    TE.flatMap((response) =>\n      TE.tryCatch(\n        () => response.json(),\n        (reason) => new Error(`JSON parse error: ${reason}`)\n      )\n    ),\n    TE.flatMap((data) => TE.fromEither(validateUser(data)))\n  );\n\nconst fetchUserPosts = (userId: string): TE.TaskEither<Error, Post[]> =>\n  pipe(\n    TE.tryCatch(\n      () => fetch(`/api/users/${userId}/posts`),\n      (reason) => new Error(`Network error: ${reason}`)\n    ),\n    TE.flatMap((response) =>\n      response.ok\n        ? TE.right(response)\n        : TE.left(new Error(`HTTP error: ${response.status}`))\n    ),\n    TE.flatMap((response) =>\n      TE.tryCatch(\n        () => response.json(),\n        (reason) => new Error(`JSON parse error: ${reason}`)\n      )\n    )\n  );\n\n// Clean composition with automatic error propagation\nconst getUserWithPosts = (\n  id: string\n): TE.TaskEither<Error, { user: User; posts: Post[] }> =>\n  pipe(\n    TE.Do,\n    TE.bind('user', () => fetchUser(id)),\n    TE.bind('posts', () => fetchUserPosts(id))\n  );\n\n// Execute and handle results\nconst main = async () => {\n  const result = await getUserWithPosts('123')();\n  pipe(\n    result,\n    E.match(\n      (error) => console.error('Failed:', error.message),\n      ({ user, posts }) => console.log('Success:', user, posts)\n    )\n  );\n};\n```\n\n### Helper: tryCatch Utility\n\nCreate a reusable wrapper for functions that might throw:\n\n```typescript\nimport * as E from 'fp-ts/Either';\nimport * as TE from 'fp-ts/TaskEither';\n\n// For sync functions\nconst tryCatchSync = <A>(f: () => A): E.Either<Error, A> =>\n  E.tryCatch(f, (e) => (e instanceof Error ? e : new Error(String(e))));\n\n// For async functions\nconst tryCatchAsync = <A>(f: () => Promise<A>): TE.TaskEither<Error, A> =>\n  TE.tryCatch(f, (e) => (e instanceof Error ? e : new Error(String(e))));\n```\n\n---\n\n## 2. Converting null checks to Option\n\n### The Problem with null/undefined\n\n- TypeScript's strict null checks help, but null still spreads through code\n- Chained property access requires verbose null guards\n- The distinction between \"missing\" and \"present but null\" is unclear\n- Easy to forget null checks leading to runtime errors\n\n### Pattern: Simple null checks to Option\n\n#### Before (Imperative)\n\n```typescript\ninterface Config {\n  database?: {\n    host?: string;\n    port?: number;\n    credentials?: {\n      username?: string;\n      password?: string;\n    };\n  };\n}\n\nfunction getDatabaseUrl(config: Config): string | null {\n  if (!config.database) {\n    return null;\n  }\n  if (!config.database.host) {\n    return null;\n  }\n  const port = config.database.port ?? 5432;\n\n  let auth = '';\n  if (config.database.credentials) {\n    if (config.database.credentials.username && config.database.credentials.password) {\n      auth = `${config.database.credentials.username}:${config.database.credentials.password}@`;\n    }\n  }\n\n  return `postgres://${auth}${config.database.host}:${port}`;\n}\n\n// Usage requires null check\nconst url = getDatabaseUrl(config);\nif (url !== null) {\n  connectToDatabase(url);\n} else {\n  console.error('Database URL not configured');\n}\n```\n\n#### After (fp-ts Option)\n\n```typescript\nimport * as O from 'fp-ts/Option';\nimport { pipe } from 'fp-ts/function';\n\nconst getDatabaseUrl = (config: Config): O.Option<string> =>\n  pipe(\n    O.fromNullable(config.database),\n    O.flatMap((db) =>\n      pipe(\n        O.fromNullable(db.host),\n        O.map((host) => {\n          const port = db.port ?? 5432;\n          const auth = pipe(\n            O.fromNullable(db.credentials),\n            O.flatMap((creds) =>\n              pipe(\n                O.Do,\n                O.bind('username', () => O.fromNullable(creds.username)),\n                O.bind('password', () => O.fromNullable(creds.password)),\n                O.map(({ username, password }) => `${username}:${password}@`)\n              )\n            ),\n            O.getOrElse(() => '')\n          );\n          return `postgres://${auth}${host}:${port}`;\n        })\n      )\n    )\n  );\n\n// Usage is explicit about the optional nature\npipe(\n  getDatabaseUrl(config),\n  O.match(\n    () => console.error('Database URL not configured'),\n    (url) => connectToDatabase(url)\n  )\n);\n```\n\n### Pattern: Array find operations\n\n#### Before (Imperative)\n\n```typescript\ninterface User {\n  id: string;\n  name: string;\n  email: string;\n}\n\nfunction findUserById(users: User[], id: string): User | undefined {\n  return users.find((u) => u.id === id);\n}\n\nfunction getUserEmail(users: User[], id: string): string | null {\n  const user = findUserById(users, id);\n  if (!user) {\n    return null;\n  }\n  return user.email;\n}\n\n// Chained lookups get messy\nfunction getManagerEmail(users: User[], employee: { managerId?: string }): string | null {\n  if (!employee.managerId) {\n    return null;\n  }\n  const manager = findUserById(users, employee.managerId);\n  if (!manager) {\n    return null;\n  }\n  return manager.email;\n}\n```\n\n#### After (fp-ts Option)\n\n```typescript\nimport * as O from 'fp-ts/Option';\nimport * as A from 'fp-ts/Array';\nimport { pipe } from 'fp-ts/function';\n\nconst findUserById = (users: User[], id: string): O.Option<User> =>\n  A.findFirst<User>((u) => u.id === id)(users);\n\nconst getUserEmail = (users: User[], id: string): O.Option<string> =>\n  pipe(\n    findUserById(users, id),\n    O.map((user) => user.email)\n  );\n\nconst getManagerEmail = (\n  users: User[],\n  employee: { managerId?: string }\n): O.Option<string> =>\n  pipe(\n    O.fromNullable(employee.managerId),\n    O.flatMap((managerId) => findUserById(users, managerId)),\n    O.map((manager) => manager.email)\n  );\n```\n\n### Step-by-Step Refactoring Guide\n\n1. **Identify nullable values**: Find all `T | null`, `T | undefined`, or optional properties\n2. **Wrap with fromNullable**: Convert nullable values to Option at system boundaries\n3. **Change return types**: From `T | null` to `Option<T>`\n4. **Replace null checks**: Use `O.map`, `O.flatMap`, `O.filter` instead of if statements\n5. **Handle at boundaries**: Use `O.getOrElse`, `O.match`, or `O.toNullable` when interfacing with non-fp code\n\n### Converting Between Option and Either\n\n```typescript\nimport * as O from 'fp-ts/Option';\nimport * as E from 'fp-ts/Either';\nimport { pipe } from 'fp-ts/function';\n\n// Option to Either: provide error for None case\nconst optionToEither = <E, A>(onNone: () => E) => (\n  option: O.Option<A>\n): E.Either<E, A> =>\n  pipe(\n    option,\n    E.fromOption(onNone)\n  );\n\n// Example\nconst findUser = (id: string): O.Option<User> => /* ... */;\n\nconst getUser = (id: string): E.Either<Error, User> =>\n  pipe(\n    findUser(id),\n    E.fromOption(() => new Error(`User ${id} not found`))\n  );\n```\n\n---\n\n## 3. Converting callbacks to Task\n\n### The Problem with Callbacks\n\n- Callback hell makes code hard to read\n- Error handling is inconsistent\n- Difficult to compose and sequence\n- No standard way to handle async operations\n\n### Pattern: Node-style callbacks to Task\n\n#### Before (Imperative)\n\n```typescript\nimport * as fs from 'fs';\n\nfunction readFileCallback(\n  path: string,\n  callback: (error: Error | null, data: string | null) => void\n): void {\n  fs.readFile(path, 'utf-8', (err, data) => {\n    if (err) {\n      callback(err, null);\n    } else {\n      callback(null, data);\n    }\n  });\n}\n\nfunction processFile(\n  inputPath: string,\n  outputPath: string,\n  callback: (error: Error | null) => void\n): void {\n  readFileCallback(inputPath, (err, data) => {\n    if (err) {\n      callback(err);\n      return;\n    }\n    const processed = data!.toUpperCase();\n    fs.writeFile(outputPath, processed, (writeErr) => {\n      if (writeErr) {\n        callback(writeErr);\n      } else {\n        callback(null);\n      }\n    });\n  });\n}\n\n// Callback hell\nfunction processMultipleFiles(\n  files: Array<{ input: string; output: string }>,\n  callback: (error: Error | null) => void\n): void {\n  let completed = 0;\n  let hasError = false;\n\n  files.forEach(({ input, output }) => {\n    if (hasError) return;\n    processFile(input, output, (err) => {\n      if (hasError) return;\n      if (err) {\n        hasError = true;\n        callback(err);\n        return;\n      }\n      completed++;\n      if (completed === files.length) {\n        callback(null);\n      }\n    });\n  });\n}\n```\n\n#### After (fp-ts Task/TaskEither)\n\n```typescript\nimport * as fs from 'fs/promises';\nimport * as TE from 'fp-ts/TaskEither';\nimport * as A from 'fp-ts/Array';\nimport { pipe } from 'fp-ts/function';\n\n// Wrap fs.promises in TaskEither\nconst readFile = (path: string): TE.TaskEither<Error, string> =>\n  TE.tryCatch(\n    () => fs.readFile(path, 'utf-8'),\n    (e) => (e instanceof Error ? e : new Error(String(e)))\n  );\n\nconst writeFile = (path: string, data: string): TE.TaskEither<Error, void> =>\n  TE.tryCatch(\n    () => fs.writeFile(path, data),\n    (e) => (e instanceof Error ? e : new Error(String(e)))\n  );\n\n// Clean composition\nconst processFile = (\n  inputPath: string,\n  outputPath: string\n): TE.TaskEither<Error, void> =>\n  pipe(\n    readFile(inputPath),\n    TE.map((data) => data.toUpperCase()),\n    TE.flatMap((processed) => writeFile(outputPath, processed))\n  );\n\n// Process multiple files in parallel or sequence\nconst processMultipleFilesParallel = (\n  files: Array<{ input: string; output: string }>\n): TE.TaskEither<Error, void[]> =>\n  pipe(\n    files,\n    A.traverse(TE.ApplicativePar)(({ input, output }) =>\n      processFile(input, output)\n    )\n  );\n\nconst processMultipleFilesSequential = (\n  files: Array<{ input: string; output: string }>\n): TE.TaskEither<Error, void[]> =>\n  pipe(\n    files,\n    A.traverse(TE.ApplicativeSeq)(({ input, output }) =>\n      processFile(input, output)\n    )\n  );\n```\n\n### Pattern: Converting callback-based APIs\n\n```typescript\nimport * as TE from 'fp-ts/TaskEither';\n\n// Generic callback-to-TaskEither converter\nconst fromCallback = <A>(\n  f: (callback: (error: Error | null, result: A | null) => void) => void\n): TE.TaskEither<Error, A> =>\n  () =>\n    new Promise((resolve) => {\n      f((error, result) => {\n        if (error) {\n          resolve({ _tag: 'Left', left: error });\n        } else {\n          resolve({ _tag: 'Right', right: result as A });\n        }\n      });\n    });\n\n// Usage\nconst readFileLegacy = (path: string): TE.TaskEither<Error, string> =>\n  fromCallback((cb) => fs.readFile(path, 'utf-8', cb));\n```\n\n---\n\n## 4. Converting class-based DI to Reader\n\n### The Problem with Class-based DI\n\n- Tight coupling between classes and their dependencies\n- Testing requires mocking entire class hierarchies\n- Dependency injection containers add runtime complexity\n- Hard to trace data flow through the application\n\n### Pattern: Service classes to Reader\n\n#### Before (Imperative with Classes)\n\n```typescript\n// Traditional class-based approach\ninterface Logger {\n  log(message: string): void;\n  error(message: string): void;\n}\n\ninterface UserRepository {\n  findById(id: string): Promise<User | null>;\n  save(user: User): Promise<void>;\n}\n\ninterface EmailService {\n  send(to: string, subject: string, body: string): Promise<void>;\n}\n\nclass UserService {\n  constructor(\n    private readonly logger: Logger,\n    private readonly userRepo: UserRepository,\n    private readonly emailService: EmailService\n  ) {}\n\n  async updateEmail(userId: string, newEmail: string): Promise<void> {\n    this.logger.log(`Updating email for user ${userId}`);\n\n    const user = await this.userRepo.findById(userId);\n    if (!user) {\n      this.logger.error(`User ${userId} not found`);\n      throw new Error(`User ${userId} not found`);\n    }\n\n    const oldEmail = user.email;\n    user.email = newEmail;\n\n    await this.userRepo.save(user);\n\n    await this.emailService.send(\n      oldEmail,\n      'Email Changed',\n      `Your email has been changed to ${newEmail}`\n    );\n\n    this.logger.log(`Email updated for user ${userId}`);\n  }\n}\n\n// Manual DI setup\nconst logger = new ConsoleLogger();\nconst userRepo = new PostgresUserRepository(dbConnection);\nconst emailService = new SmtpEmailService(smtpConfig);\nconst userService = new UserService(logger, userRepo, emailService);\n```\n\n#### After (fp-ts Reader)\n\n```typescript\nimport * as R from 'fp-ts/Reader';\nimport * as RTE from 'fp-ts/ReaderTaskEither';\nimport * as TE from 'fp-ts/TaskEither';\nimport { pipe } from 'fp-ts/function';\n\n// Define the environment/dependencies as an interface\ninterface AppEnv {\n  logger: {\n    log: (message: string) => void;\n    error: (message: string) => void;\n  };\n  userRepo: {\n    findById: (id: string) => TE.TaskEither<Error, User | null>;\n    save: (user: User) => TE.TaskEither<Error, void>;\n  };\n  emailService: {\n    send: (to: string, subject: string, body: string) => TE.TaskEither<Error, void>;\n  };\n}\n\n// Helper to access environment\nconst ask = RTE.ask<AppEnv, Error>();\n\n// Service functions using ReaderTaskEither\nconst logInfo = (message: string): RTE.ReaderTaskEither<AppEnv, Error, void> =>\n  pipe(\n    ask,\n    RTE.map((env) => env.logger.log(message))\n  );\n\nconst logError = (message: string): RTE.ReaderTaskEither<AppEnv, Error, void> =>\n  pipe(\n    ask,\n    RTE.map((env) => env.logger.error(message))\n  );\n\nconst findUser = (id: string): RTE.ReaderTaskEither<AppEnv, Error, User | null> =>\n  pipe(\n    ask,\n    RTE.flatMapTaskEither((env) => env.userRepo.findById(id))\n  );\n\nconst saveUser = (user: User): RTE.ReaderTaskEither<AppEnv, Error, void> =>\n  pipe(\n    ask,\n    RTE.flatMapTaskEither((env) => env.userRepo.save(user))\n  );\n\nconst sendEmail = (\n  to: string,\n  subject: string,\n  body: string\n): RTE.ReaderTaskEither<AppEnv, Error, void> =>\n  pipe(\n    ask,\n    RTE.flatMapTaskEither((env) => env.emailService.send(to, subject, body))\n  );\n\n// The updateEmail function using Reader composition\nconst updateEmail = (\n  userId: string,\n  newEmail: string\n): RTE.ReaderTaskEither<AppEnv, Error, void> =>\n  pipe(\n    logInfo(`Updating email for user ${userId}`),\n    RTE.flatMap(() => findUser(userId)),\n    RTE.flatMap((user) => {\n      if (!user) {\n        return pipe(\n          logError(`User ${userId} not found`),\n          RTE.flatMap(() => RTE.left(new Error(`User ${userId} not found`)))\n        );\n      }\n      const oldEmail = user.email;\n      const updatedUser = { ...user, email: newEmail };\n\n      return pipe(\n        saveUser(updatedUser),\n        RTE.flatMap(() =>\n          sendEmail(\n            oldEmail,\n            'Email Changed',\n            `Your email has been changed to ${newEmail}`\n          )\n        ),\n        RTE.flatMap(() => logInfo(`Email updated for user ${userId}`))\n      );\n    })\n  );\n\n// Build the environment\nconst createAppEnv = (): AppEnv => ({\n  logger: {\n    log: (msg) => console.log(`[INFO] ${msg}`),\n    error: (msg) => console.error(`[ERROR] ${msg}`),\n  },\n  userRepo: {\n    findById: (id) => TE.tryCatch(\n      () => postgresClient.query('SELECT * FROM users WHERE id = $1', [id]),\n      (e) => new Error(String(e))\n    ),\n    save: (user) => TE.tryCatch(\n      () => postgresClient.query('UPDATE users SET email = $1 WHERE id = $2', [user.email, user.id]),\n      (e) => new Error(String(e))\n    ),\n  },\n  emailService: {\n    send: (to, subject, body) => TE.tryCatch(\n      () => smtpClient.send({ to, subject, body }),\n      (e) => new Error(String(e))\n    ),\n  },\n});\n\n// Run the program\nconst main = async () => {\n  const env = createAppEnv();\n  const result = await updateEmail('user-123', 'new@email.com')(env)();\n\n  pipe(\n    result,\n    E.match(\n      (error) => console.error('Failed:', error),\n      () => console.log('Success!')\n    )\n  );\n};\n```\n\n### Testing with Reader\n\n```typescript\n// Easy to test with mock environment\nconst createTestEnv = (): AppEnv => {\n  const logs: string[] = [];\n  const savedUsers: User[] = [];\n  const sentEmails: Array<{ to: string; subject: string; body: string }> = [];\n\n  return {\n    logger: {\n      log: (msg) => logs.push(`[INFO] ${msg}`),\n      error: (msg) => logs.push(`[ERROR] ${msg}`),\n    },\n    userRepo: {\n      findById: (id) =>\n        TE.right(id === 'existing-user' ? { id, email: 'old@email.com', name: 'Test' } : null),\n      save: (user) => {\n        savedUsers.push(user);\n        return TE.right(undefined);\n      },\n    },\n    emailService: {\n      send: (to, subject, body) => {\n        sentEmails.push({ to, subject, body });\n        return TE.right(undefined);\n      },\n    },\n  };\n};\n\n// Test\ndescribe('updateEmail', () => {\n  it('should update email and send notification', async () => {\n    const env = createTestEnv();\n    const result = await updateEmail('existing-user', 'new@email.com')(env)();\n\n    expect(E.isRight(result)).toBe(true);\n    // Assert on captured side effects\n  });\n});\n```\n\n---\n\n## 5. Converting imperative loops to functional operations\n\n### Pattern: for loops to map/filter/reduce\n\n#### Before (Imperative)\n\n```typescript\ninterface Product {\n  id: string;\n  name: string;\n  price: number;\n  category: string;\n  inStock: boolean;\n}\n\nfunction processProducts(products: Product[]): {\n  totalValue: number;\n  categoryCounts: Record<string, number>;\n  expensiveProducts: string[];\n} {\n  let totalValue = 0;\n  const categoryCounts: Record<string, number> = {};\n  const expensiveProducts: string[] = [];\n\n  for (let i = 0; i < products.length; i++) {\n    const product = products[i];\n\n    // Skip out of stock\n    if (!product.inStock) {\n      continue;\n    }\n\n    // Sum total value\n    totalValue += product.price;\n\n    // Count categories\n    if (categoryCounts[product.category] === undefined) {\n      categoryCounts[product.category] = 0;\n    }\n    categoryCounts[product.category]++;\n\n    // Collect expensive products\n    if (product.price > 100) {\n      expensiveProducts.push(product.name);\n    }\n  }\n\n  return { totalValue, categoryCounts, expensiveProducts };\n}\n```\n\n#### After (fp-ts functional operations)\n\n```typescript\nimport * as A from 'fp-ts/Array';\nimport * as R from 'fp-ts/Record';\nimport { pipe } from 'fp-ts/function';\nimport * as N from 'fp-ts/number';\nimport * as Monoid from 'fp-ts/Monoid';\n\nconst processProducts = (products: Product[]) => {\n  const inStockProducts = pipe(\n    products,\n    A.filter((p) => p.inStock)\n  );\n\n  const totalValue = pipe(\n    inStockProducts,\n    A.map((p) => p.price),\n    A.reduce(0, (acc, price) => acc + price)\n  );\n\n  const categoryCounts = pipe(\n    inStockProducts,\n    A.reduce({} as Record<string, number>, (acc, product) => ({\n      ...acc,\n      [product.category]: (acc[product.category] ?? 0) + 1,\n    }))\n  );\n\n  const expensiveProducts = pipe(\n    inStockProducts,\n    A.filter((p) => p.price > 100),\n    A.map((p) => p.name)\n  );\n\n  return { totalValue, categoryCounts, expensiveProducts };\n};\n\n// Or using a single pass with foldMap for efficiency\nimport { Monoid as M } from 'fp-ts/Monoid';\n\ninterface ProductStats {\n  totalValue: number;\n  categoryCounts: Record<string, number>;\n  expensiveProducts: string[];\n}\n\nconst productStatsMonoid: M<ProductStats> = {\n  empty: { totalValue: 0, categoryCounts: {}, expensiveProducts: [] },\n  concat: (a, b) => ({\n    totalValue: a.totalValue + b.totalValue,\n    categoryCounts: pipe(\n      a.categoryCounts,\n      R.union({ concat: (x, y) => x + y })(b.categoryCounts)\n    ),\n    expensiveProducts: [...a.expensiveProducts, ...b.expensiveProducts],\n  }),\n};\n\nconst processProductsSinglePass = (products: Product[]): ProductStats =>\n  pipe(\n    products,\n    A.filter((p) => p.inStock),\n    A.foldMap(productStatsMonoid)((product) => ({\n      totalValue: product.price,\n      categoryCounts: { [product.category]: 1 },\n      expensiveProducts: product.price > 100 ? [product.name] : [],\n    }))\n  );\n```\n\n### Pattern: Nested loops to flatMap\n\n#### Before (Imperative)\n\n```typescript\ninterface Order {\n  id: string;\n  items: OrderItem[];\n}\n\ninterface OrderItem {\n  productId: string;\n  quantity: number;\n}\n\nfunction getAllProductIds(orders: Order[]): string[] {\n  const productIds: string[] = [];\n\n  for (const order of orders) {\n    for (const item of order.items) {\n      if (!productIds.includes(item.productId)) {\n        productIds.push(item.productId);\n      }\n    }\n  }\n\n  return productIds;\n}\n```\n\n#### After (fp-ts)\n\n```typescript\nimport * as A from 'fp-ts/Array';\nimport { pipe } from 'fp-ts/function';\nimport * as S from 'fp-ts/Set';\nimport * as Str from 'fp-ts/string';\n\nconst getAllProductIds = (orders: Order[]): string[] =>\n  pipe(\n    orders,\n    A.flatMap((order) => order.items),\n    A.map((item) => item.productId),\n    A.uniq(Str.Eq)\n  );\n\n// Or using Set for better performance with large datasets\nconst getAllProductIdsSet = (orders: Order[]): Set<string> =>\n  pipe(\n    orders,\n    A.flatMap((order) => order.items),\n    A.map((item) => item.productId),\n    (ids) => new Set(ids)\n  );\n```\n\n### Pattern: while loops to recursion/unfold\n\n#### Before (Imperative)\n\n```typescript\nfunction paginate<T>(\n  fetchPage: (cursor: string | null) => Promise<{ items: T[]; nextCursor: string | null }>\n): Promise<T[]> {\n  const allItems: T[] = [];\n  let cursor: string | null = null;\n\n  while (true) {\n    const { items, nextCursor } = await fetchPage(cursor);\n    allItems.push(...items);\n\n    if (nextCursor === null) {\n      break;\n    }\n    cursor = nextCursor;\n  }\n\n  return allItems;\n}\n```\n\n#### After (fp-ts)\n\n```typescript\nimport * as TE from 'fp-ts/TaskEither';\nimport * as A from 'fp-ts/Array';\nimport { pipe } from 'fp-ts/function';\n\ninterface Page<T> {\n  items: T[];\n  nextCursor: string | null;\n}\n\nconst paginate = <T>(\n  fetchPage: (cursor: string | null) => TE.TaskEither<Error, Page<T>>\n): TE.TaskEither<Error, T[]> => {\n  const go = (\n    cursor: string | null,\n    accumulated: T[]\n  ): TE.TaskEither<Error, T[]> =>\n    pipe(\n      fetchPage(cursor),\n      TE.flatMap(({ items, nextCursor }) => {\n        const newAccumulated = [...accumulated, ...items];\n        return nextCursor === null\n          ? TE.right(newAccumulated)\n          : go(nextCursor, newAccumulated);\n      })\n    );\n\n  return go(null, []);\n};\n\n// Using unfold for generating sequences\nimport * as RA from 'fp-ts/ReadonlyArray';\n\nconst range = (start: number, end: number): readonly number[] =>\n  RA.unfold(start, (n) => (n <= end ? O.some([n, n + 1]) : O.none));\n```\n\n---\n\n## 6. Migrating Promise chains to TaskEither\n\n### Pattern: Promise.then chains to pipe\n\n#### Before (Imperative)\n\n```typescript\nfunction fetchUserData(userId: string): Promise<UserProfile> {\n  return fetch(`/api/users/${userId}`)\n    .then((response) => {\n      if (!response.ok) {\n        throw new Error(`HTTP ${response.status}`);\n      }\n      return response.json();\n    })\n    .then((data) => validateUserData(data))\n    .then((validData) => enrichUserProfile(validData))\n    .catch((error) => {\n      console.error('Failed to fetch user data:', error);\n      throw error;\n    });\n}\n\n// Chained promises with conditionals\nfunction processOrder(orderId: string): Promise<OrderResult> {\n  return getOrder(orderId)\n    .then((order) => {\n      if (order.status === 'cancelled') {\n        throw new Error('Order is cancelled');\n      }\n      return order;\n    })\n    .then((order) => validateInventory(order))\n    .then((validOrder) => processPayment(validOrder))\n    .then((paidOrder) => shipOrder(paidOrder))\n    .catch((error) => {\n      logError(error);\n      return { success: false, error: error.message };\n    });\n}\n```\n\n#### After (fp-ts TaskEither)\n\n```typescript\nimport * as TE from 'fp-ts/TaskEither';\nimport * as E from 'fp-ts/Either';\nimport { pipe } from 'fp-ts/function';\n\nconst fetchUserData = (userId: string): TE.TaskEither<Error, UserProfile> =>\n  pipe(\n    TE.tryCatch(\n      () => fetch(`/api/users/${userId}`),\n      (e) => new Error(`Network error: ${e}`)\n    ),\n    TE.flatMap((response) =>\n      response.ok\n        ? TE.tryCatch(\n            () => response.json(),\n            (e) => new Error(`Parse error: ${e}`)\n          )\n        : TE.left(new Error(`HTTP ${response.status}`))\n    ),\n    TE.flatMap((data) => TE.fromEither(validateUserData(data))),\n    TE.flatMap((validData) => enrichUserProfile(validData))\n  );\n\n// Conditionals are explicit\nconst processOrder = (orderId: string): TE.TaskEither<Error, OrderResult> =>\n  pipe(\n    getOrder(orderId),\n    TE.filterOrElse(\n      (order) => order.status !== 'cancelled',\n      () => new Error('Order is cancelled')\n    ),\n    TE.flatMap(validateInventory),\n    TE.flatMap(processPayment),\n    TE.flatMap(shipOrder),\n    TE.map((shipped) => ({ success: true, order: shipped })),\n    TE.orElse((error) =>\n      pipe(\n        TE.fromIO(() => logError(error)),\n        TE.map(() => ({ success: false, error: error.message }))\n      )\n    )\n  );\n```\n\n### Pattern: Promise.all to traverse\n\n#### Before (Imperative)\n\n```typescript\nasync function fetchAllUsers(ids: string[]): Promise<User[]> {\n  const promises = ids.map((id) => fetchUser(id));\n  return Promise.all(promises);\n}\n\n// With error handling for individual items\nasync function fetchUsersWithFallback(ids: string[]): Promise<Array<User | null>> {\n  const promises = ids.map(async (id) => {\n    try {\n      return await fetchUser(id);\n    } catch {\n      return null;\n    }\n  });\n  return Promise.all(promises);\n}\n```\n\n#### After (fp-ts)\n\n```typescript\nimport * as TE from 'fp-ts/TaskEither';\nimport * as A from 'fp-ts/Array';\nimport * as T from 'fp-ts/Task';\nimport { pipe } from 'fp-ts/function';\n\n// Parallel execution - fails fast on first error\nconst fetchAllUsers = (ids: string[]): TE.TaskEither<Error, User[]> =>\n  pipe(\n    ids,\n    A.traverse(TE.ApplicativePar)(fetchUser)\n  );\n\n// Sequential execution\nconst fetchAllUsersSequential = (ids: string[]): TE.TaskEither<Error, User[]> =>\n  pipe(\n    ids,\n    A.traverse(TE.ApplicativeSeq)(fetchUser)\n  );\n\n// Collect successes, ignore failures (using Task instead of TaskEither)\nconst fetchUsersWithFallback = (ids: string[]): T.Task<Array<User | null>> =>\n  pipe(\n    ids,\n    A.traverse(T.ApplicativePar)((id) =>\n      pipe(\n        fetchUser(id),\n        TE.match(\n          () => null,\n          (user) => user\n        )\n      )\n    )\n  );\n\n// Or keep track of which failed\nconst fetchUsersPartitioned = (\n  ids: string[]\n): T.Task<{ successes: User[]; failures: Array<{ id: string; error: Error }> }> =>\n  pipe(\n    ids,\n    A.traverse(T.ApplicativePar)((id) =>\n      pipe(\n        fetchUser(id),\n        TE.bimap(\n          (error) => ({ id, error }),\n          (user) => user\n        ),\n        (te) => te\n      )\n    ),\n    T.map(A.separate),\n    T.map(({ left: failures, right: successes }) => ({ successes, failures }))\n  );\n```\n\n### Pattern: Promise.race to alternative\n\n```typescript\nimport * as TE from 'fp-ts/TaskEither';\nimport * as T from 'fp-ts/Task';\nimport { pipe } from 'fp-ts/function';\n\n// Race - first to complete wins\nconst raceTaskEithers = <E, A>(\n  tasks: Array<TE.TaskEither<E, A>>\n): TE.TaskEither<E, A> =>\n  () => Promise.race(tasks.map((te) => te()));\n\n// Try alternatives on failure (like Promise.any but typed)\nconst tryAlternatives = <E, A>(\n  primary: TE.TaskEither<E, A>,\n  fallback: TE.TaskEither<E, A>\n): TE.TaskEither<E, A> =>\n  pipe(\n    primary,\n    TE.orElse(() => fallback)\n  );\n\n// Chain of fallbacks\nconst withFallbacks = <E, A>(\n  tasks: Array<TE.TaskEither<E, A>>\n): TE.TaskEither<E, A> =>\n  tasks.reduce((acc, task) => pipe(acc, TE.orElse(() => task)));\n```\n\n---\n\n## 7. Common Pitfalls\n\n### Pitfall 1: Forgetting to run Tasks\n\n```typescript\n// WRONG: Task is not executed\nconst fetchData = (): TE.TaskEither<Error, Data> => /* ... */;\nconst result = fetchData(); // This is still a Task, not the result!\n\n// CORRECT: Execute the Task\nconst result = await fetchData()(); // Note the double invocation\n```\n\n### Pitfall 2: Mixing async/await with fp-ts incorrectly\n\n```typescript\n// WRONG: Breaking out of the fp-ts ecosystem\nconst processData = async (input: string): Promise<Result> => {\n  const parsed = parseInput(input); // Returns Either\n  if (E.isLeft(parsed)) {\n    throw new Error(parsed.left.message); // Don't do this!\n  }\n  return await fetchData(parsed.right)();\n};\n\n// CORRECT: Stay in the ecosystem\nconst processData = (input: string): TE.TaskEither<Error, Result> =>\n  pipe(\n    parseInput(input),\n    TE.fromEither,\n    TE.flatMap(fetchData)\n  );\n```\n\n### Pitfall 3: Using map when flatMap is needed\n\n```typescript\n// WRONG: Results in nested Either\nconst result: E.Either<Error, E.Either<Error, User>> = pipe(\n  parseUserId(input), // E.Either<Error, string>\n  E.map(fetchUser) // Returns E.Either<Error, User>, so we get nested Either\n);\n\n// CORRECT: Use flatMap to flatten\nconst result: E.Either<Error, User> = pipe(\n  parseUserId(input),\n  E.flatMap(fetchUser)\n);\n```\n\n### Pitfall 4: Losing error information\n\n```typescript\n// WRONG: Original error context is lost\nconst fetchData = (): TE.TaskEither<Error, Data> =>\n  pipe(\n    TE.tryCatch(\n      () => fetch('/api/data'),\n      () => new Error('Failed') // Lost the original error!\n    )\n  );\n\n// CORRECT: Preserve error context\nconst fetchData = (): TE.TaskEither<Error, Data> =>\n  pipe(\n    TE.tryCatch(\n      () => fetch('/api/data'),\n      (reason) => new Error(`Network request failed: ${reason}`)\n    )\n  );\n\n// BETTER: Use typed errors\ntype FetchError =\n  | { _tag: 'NetworkError'; cause: unknown }\n  | { _tag: 'ParseError'; cause: unknown }\n  | { _tag: 'ValidationError'; message: string };\n\nconst fetchData = (): TE.TaskEither<FetchError, Data> =>\n  pipe(\n    TE.tryCatch(\n      () => fetch('/api/data'),\n      (cause): FetchError => ({ _tag: 'NetworkError', cause })\n    ),\n    TE.flatMap((response) =>\n      TE.tryCatch(\n        () => response.json(),\n        (cause): FetchError => ({ _tag: 'ParseError', cause })\n      )\n    )\n  );\n```\n\n### Pitfall 5: Overusing fromNullable\n\n```typescript\n// WRONG: Unnecessary wrapping and unwrapping\nconst getName = (user: User | null): string => {\n  const optUser = O.fromNullable(user);\n  const name = pipe(optUser, O.map(u => u.name), O.toNullable);\n  return name ?? 'Unknown';\n};\n\n// CORRECT: Use Option only when you need its composition benefits\nconst getName = (user: User | null): string => user?.name ?? 'Unknown';\n\n// BETTER: Use Option when chaining multiple operations\nconst getManagerName = (user: User | null): O.Option<string> =>\n  pipe(\n    O.fromNullable(user),\n    O.flatMap(u => O.fromNullable(u.manager)),\n    O.map(m => m.name)\n  );\n```\n\n### Pitfall 6: Not handling the left case\n\n```typescript\n// WRONG: Ignoring potential errors\nconst processUser = (input: string): User => {\n  const result = parseUser(input); // E.Either<Error, User>\n  return (result as E.Right<User>).right; // Unsafe cast!\n};\n\n// CORRECT: Always handle both cases\nconst processUser = (input: string): User =>\n  pipe(\n    parseUser(input),\n    E.getOrElse((error) => {\n      console.error('Parse failed:', error);\n      return defaultUser;\n    })\n  );\n```\n\n---\n\n## 8. Gradual Adoption Strategies\n\n### Strategy 1: Start at the Boundaries\n\nBegin by converting functions at the edges of your system:\n- API response handlers\n- Database query results\n- File system operations\n- User input validation\n\n```typescript\n// Wrap external API calls first\nconst fetchUserApi = (id: string): TE.TaskEither<ApiError, UserDto> =>\n  pipe(\n    TE.tryCatch(\n      () => externalApiClient.getUser(id),\n      (e) => ({ type: 'api_error' as const, cause: e })\n    )\n  );\n\n// Internal code can stay imperative initially\nasync function handleUserRequest(userId: string) {\n  const result = await fetchUserApi(userId)();\n  if (E.isRight(result)) {\n    // Process user with existing code\n    return processUser(result.right);\n  } else {\n    throw new Error(`API error: ${result.left.type}`);\n  }\n}\n```\n\n### Strategy 2: Create Bridge Functions\n\nBuild helpers to convert between fp-ts and imperative code:\n\n```typescript\n// Bridge from Either to thrown errors\nconst unsafeUnwrap = <E, A>(either: E.Either<E, A>): A =>\n  pipe(\n    either,\n    E.getOrElseW((e) => {\n      throw e instanceof Error ? e : new Error(String(e));\n    })\n  );\n\n// Bridge from thrown errors to Either\nconst catchSync = <A>(f: () => A): E.Either<Error, A> =>\n  E.tryCatch(f, (e) => (e instanceof Error ? e : new Error(String(e))));\n\n// Bridge from Promise to TaskEither\nconst fromPromise = <A>(p: Promise<A>): TE.TaskEither<Error, A> =>\n  TE.tryCatch(() => p, (e) => (e instanceof Error ? e : new Error(String(e))));\n\n// Bridge from TaskEither to Promise (throws on Left)\nconst toPromise = <E, A>(te: TE.TaskEither<E, A>): Promise<A> =>\n  te().then(E.getOrElseW((e) => { throw e; }));\n```\n\n### Strategy 3: Module-by-Module Migration\n\n1. **Pick a module** with clear boundaries\n2. **Add fp-ts types** to internal functions\n3. **Keep external API unchanged** initially\n4. **Test thoroughly** before moving on\n5. **Update external API** once internals are stable\n\n```typescript\n// Phase 1: Internal functions use fp-ts\n// File: user-service.internal.ts\nexport const validateUser = (data: unknown): E.Either<ValidationError, User> => /* ... */;\nexport const enrichUser = (user: User): TE.TaskEither<Error, EnrichedUser> => /* ... */;\n\n// File: user-service.ts (public API unchanged)\nexport async function getUser(id: string): Promise<User> {\n  const result = await pipe(\n    fetchUser(id),\n    TE.flatMap(validateUser >>> TE.fromEither),\n    TE.flatMap(enrichUser)\n  )();\n\n  if (E.isLeft(result)) {\n    throw result.left;\n  }\n  return result.right;\n}\n\n// Phase 2: Update public API\n// File: user-service.ts\nexport const getUser = (id: string): TE.TaskEither<UserError, User> =>\n  pipe(\n    fetchUser(id),\n    TE.flatMap(validateUser >>> TE.fromEither),\n    TE.flatMap(enrichUser)\n  );\n```\n\n### Strategy 4: Type-Driven Development\n\nUse TypeScript's type system to guide the migration:\n\n```typescript\n// Step 1: Change type signature first\ntype OldGetUser = (id: string) => Promise<User | null>;\ntype NewGetUser = (id: string) => TE.TaskEither<UserError, User>;\n\n// Step 2: Compiler will show all call sites that need updating\nconst getUser: NewGetUser = (id) => /* implement */;\n\n// Step 3: Update call sites one by one\n// The compiler ensures you handle all cases\n```\n\n### Strategy 5: Testing as Documentation\n\nWrite tests that demonstrate the expected behavior:\n\n```typescript\ndescribe('UserService', () => {\n  describe('getUser (fp-ts)', () => {\n    it('returns Right with user on success', async () => {\n      const result = await getUser('valid-id')();\n      expect(E.isRight(result)).toBe(true);\n      if (E.isRight(result)) {\n        expect(result.right.id).toBe('valid-id');\n      }\n    });\n\n    it('returns Left with NotFound error for unknown id', async () => {\n      const result = await getUser('unknown')();\n      expect(E.isLeft(result)).toBe(true);\n      if (E.isLeft(result)) {\n        expect(result.left._tag).toBe('NotFound');\n      }\n    });\n  });\n});\n```\n\n---\n\n## 9. When NOT to Refactor\n\n### Simple Synchronous Code\n\nDon't refactor straightforward code that doesn't benefit from fp-ts:\n\n```typescript\n// This is fine as-is\nfunction formatName(first: string, last: string): string {\n  return `${first} ${last}`;\n}\n\n// Don't do this - it adds complexity without benefit\nconst formatName = (first: string, last: string): string =>\n  pipe(\n    first,\n    (f) => `${f} ${last}`\n  );\n```\n\n### Performance-Critical Loops\n\nfp-ts operations create intermediate arrays. For hot paths, keep imperative code:\n\n```typescript\n// Keep this for performance-critical code processing millions of items\nfunction sumLargeArray(numbers: number[]): number {\n  let sum = 0;\n  for (let i = 0; i < numbers.length; i++) {\n    sum += numbers[i];\n  }\n  return sum;\n}\n\n// This creates intermediate arrays\nconst sumWithFpts = (numbers: number[]): number =>\n  pipe(numbers, A.reduce(0, (acc, n) => acc + n));\n```\n\n### Third-Party Library Interfaces\n\nWhen working with libraries that expect specific patterns:\n\n```typescript\n// Express middleware must match Express's interface\napp.get('/users/:id', async (req, res) => {\n  // Keep imperative here, convert at boundaries\n  const result = await getUser(req.params.id)();\n\n  if (E.isLeft(result)) {\n    res.status(404).json({ error: result.left.message });\n  } else {\n    res.json(result.right);\n  }\n});\n```\n\n### Code Touched by Non-FP Team Members\n\nIf your team isn't familiar with fp-ts, forced adoption will hurt productivity:\n\n```typescript\n// If team doesn't know fp-ts, this is harder to maintain\nconst processOrder = (order: Order): TE.TaskEither<Error, Result> =>\n  pipe(\n    validateOrder(order),\n    TE.fromEither,\n    TE.flatMap(enrichOrder),\n    TE.flatMap(submitOrder)\n  );\n\n// Familiar to all TypeScript developers\nasync function processOrder(order: Order): Promise<Result> {\n  const validated = validateOrder(order);\n  if (!validated.success) {\n    throw new Error(validated.error);\n  }\n  const enriched = await enrichOrder(validated.data);\n  return await submitOrder(enriched);\n}\n```\n\n### Trivial Null Checks\n\nDon't use Option for simple, one-off null checks:\n\n```typescript\n// This is fine\nconst name = user?.name ?? 'Anonymous';\n\n// Overkill for simple cases\nconst name = pipe(\n  O.fromNullable(user),\n  O.map((u) => u.name),\n  O.getOrElse(() => 'Anonymous')\n);\n```\n\n### When the Error Type Doesn't Matter\n\nIf you're going to throw/log anyway and don't need error composition:\n\n```typescript\n// If this is your error handling anyway...\ntry {\n  await doSomething();\n} catch (e) {\n  logger.error(e);\n  throw e;\n}\n\n// ...then Either doesn't add much value\nconst result = await doSomethingTE()();\nif (E.isLeft(result)) {\n  logger.error(result.left);\n  throw result.left;\n}\n```\n\n### Test Code\n\nTest code should be readable, not necessarily functional:\n\n```typescript\n// Clear test code\ndescribe('UserService', () => {\n  it('creates a user', async () => {\n    const user = await createUser({ name: 'Alice' });\n    expect(user.name).toBe('Alice');\n  });\n});\n\n// Unnecessarily complex\ndescribe('UserService', () => {\n  it('creates a user', async () => {\n    await pipe(\n      createUser({ name: 'Alice' }),\n      TE.map((user) => expect(user.name).toBe('Alice')),\n      TE.getOrElse(() => T.of(fail('Should not fail')))\n    )();\n  });\n});\n```\n\n---\n\n## Quick Reference: Imperative to fp-ts Mapping\n\n| Imperative Pattern | fp-ts Equivalent |\n|-------------------|------------------|\n| `try { } catch { }` | `E.tryCatch()`, `TE.tryCatch()` |\n| `throw new Error()` | `E.left()`, `TE.left()` |\n| `return value` | `E.right()`, `TE.right()` |\n| `if (x === null)` | `O.fromNullable()`, `O.isNone()` |\n| `x ?? defaultValue` | `O.getOrElse()` |\n| `x?.property` | `O.map()`, `O.flatMap()` |\n| `array.map()` | `A.map()` |\n| `array.filter()` | `A.filter()` |\n| `array.reduce()` | `A.reduce()`, `A.foldMap()` |\n| `array.find()` | `A.findFirst()` |\n| `array.flatMap()` | `A.flatMap()` |\n| `Promise.then()` | `TE.map()`, `TE.flatMap()` |\n| `Promise.catch()` | `TE.orElse()`, `TE.mapLeft()` |\n| `Promise.all()` | `A.traverse(TE.ApplicativePar)` |\n| `async/await` | `TE.flatMap()` chain |\n| `new Class(deps)` | `R.asks()`, `RTE.ask()` |\n| `for...of` | `A.map()`, `A.reduce()` |\n| `while` | Recursion, `unfold()` |\n\n---\n\n## Summary\n\nMigrating to fp-ts is a journey, not a destination. Key principles:\n\n1. **Start small**: Convert individual functions, not entire codebases\n2. **Be pragmatic**: Not everything needs to be functional\n3. **Type-driven**: Let the compiler guide your refactoring\n4. **Test thoroughly**: Each conversion should be verified\n5. **Document patterns**: Create team-specific guides for your codebase\n6. **Review benefits**: Ensure the added complexity provides value\n\nThe goal is more maintainable, type-safe code—not functional programming for its own sake.\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.","tags":["refactor","antigravity","awesome","skills","sickn33","agent-skills","agentic-skills","ai-agent-skills","ai-agents","ai-coding","ai-workflows","antigravity-skills"],"capabilities":["skill","source-sickn33","skill-fp-refactor","topic-agent-skills","topic-agentic-skills","topic-ai-agent-skills","topic-ai-agents","topic-ai-coding","topic-ai-workflows","topic-antigravity","topic-antigravity-skills","topic-claude-code","topic-claude-code-skills","topic-codex-cli","topic-codex-skills"],"categories":["antigravity-awesome-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/sickn33/antigravity-awesome-skills/fp-refactor","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add sickn33/antigravity-awesome-skills","source_repo":"https://github.com/sickn33/antigravity-awesome-skills","install_from":"skills.sh"}},"qualityScore":"0.700","qualityRationale":"deterministic score 0.70 from registry signals: · indexed on github topic:agent-skills · 34793 github stars · SKILL.md body (45,794 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-04-24T00:50:58.479Z","embedding":null,"createdAt":"2026-04-18T21:37:29.740Z","updatedAt":"2026-04-24T00:50:58.479Z","lastSeenAt":"2026-04-24T00:50:58.479Z","tsv":"'-123':2373 '-8':1542,1687,1858 '/api/data':3735,3755,3789 '/api/users':640,679,783,828,3066,3183 '/array':1283,1664,2601,2823,2956,3335 '/either':385,761,930,1425,3165 '/function':400,768,1121,1290,1432,1671,2082,2616,2830,2963,3172,3350,3484 '/json':393 '/monoid':2632,2706 '/number':2624 '/option':1114,1275,1417 '/posts':681,830 '/reader':2059 '/readertaskeither':2067 '/readonlyarray':3026 '/record':2609 '/set':2838 '/string':2846 '/task':3343,3477 '/taskeither':753,938,1656,1802,2075,2948,3157,3327,3469 '/users':4549 '0':1608,2532,2544,2572,2652,2672,2722,4497,4501,4522 '1':90,97,199,545,1342,2318,2333,2673,2761,3043,3559,3934,4142,4180,4275,4895 '100':2580,2681,2764 '123':896 '2':104,110,560,981,1355,2336,3599,4021,4149,4236,4295,4904 '3':117,122,576,1367,1479,3663,4136,4158,4311,4913 '30':526 '4':128,136,588,1376,1860,3716,4164,4259,4923 '404':4569 '5':144,151,598,1388,2491,3805,4170,4326,4931 '5432':1067,1140 '6':159,165,609,3045,3878,4942 '7':172,175,3555 '8':179,183,3929 '9':188,193,4402 'a.categorycounts':2733 'a.expensiveproducts':2742 'a.filter':2641,2678,2751,4849 'a.findfirst':1298,4854 'a.flatmap':2854,2878,4856 'a.foldmap':2754,4852 'a.map':2648,2682,2857,2881,4847,4876 'a.reduce':2651,2661,4521,4851,4877 'a.separate':3449 'a.totalvalue':2729 'a.traverse':1761,1781,3367,3381,3403,3434,4864 'a.uniq':2860 'acc':2653,2655,2666,2668,2670,3549,3552,4523,4525 'access':1005,2127 'accumul':2988,3001 'ad':4947 'add':1891,4150,4445,4736 'adopt':181,186,3931,4595 'age':325,333,405,486,495,525 'alic':524,4776,4780,4794,4800 'allitem':2911,2935 'allitems.push':2926 'altern':3460,3507 'alway':3909 'anonym':4680,4694 'anyway':4708,4722 'api':1793,3949,3964,3980,4017,4161,4173,4208,4239 'apierror':3972 'app.get':4548 'appenv':2090,2132,2143,2157,2171,2186,2204,2228,2296,2397 'applic':1901 'approach':1916 'appropri':557 'array':1188,1595,1751,1771,2406,3296,3398,3427,3495,3541,4471,4513 'array.filter':4848 'array.find':4853 'array.flatmap':4855 'array.map':4846 'array.reduce':4850 'as-i':4427 'ask':2130,2147,2161,2176,2190,2208,5000 'assert':2486 'async':620,629,667,709,891,961,1509,1964,2364,2468,3268,3290,3302,3619,3992,4211,4352,4383,4551,4633,4770,4789 'async/await':3601,4866 'auth':1069,1075,1079,1142,1165 'automat':504,862 'await':638,652,677,691,723,728,894,1979,2001,2004,2370,2474,2923,3306,3592,3641,3999,4219,4355,4386,4562,4651,4655,4724,4741,4773,4790 'b':2727 'b.categorycounts':2740 'b.expensiveproducts':2743 'b.totalvalue':2730 'base':132,140,1792,1864,1873,1915 'begin':3939 'behavior':4336 'benefit':3844,4418,4448,4944 'better':2866,3763,3854 'block':216,603 'bodi':1946,2120,2201,2214,2348,2353,2411,2450,2454 'boolean':2517 'boundari':1366,1391,3938,4148,4559,5008 'break':2931,3609 'bridg':4023,4037,4065,4089,4112 'build':2291,4025 'call':3965,4300,4313 'callback':67,119,125,1481,1487,1488,1515,1530,1547,1551,1560,1572,1585,1588,1590,1600,1629,1636,1791,1805,1812 'callback-bas':1790 'callback-to-taskeith':1804 'caller':611 'cancel':3114,3120,3232,3237 'captur':2488 'case':519,1440,3883,3912,4324,4684 'cast':3907 'catch':94,101,203,211,215,259,274,335,344,362,602,623,657,693,708,734,3087,3135,3309,4726,4822 'catchsync':4072 'categori':2514,2565 'categorycount':2524,2534,2567,2570,2573,2585,2658,2687,2711,2723,2731,2759 'caus':3771,3775,3790,3794,3799,3803,3984 'cb':1854,1859 'chain':162,169,617,1003,1234,3048,3053,3098,3533,3858,4868 'chang':561,1368,2008,2013,2276,2281,4276 'check':66,107,114,984,995,1024,1032,1085,1379,4660,4671 'clarif':5002 'class':131,139,1863,1872,1878,1886,1904,1910,1914,1949,4870 'class-bas':130,1862,1871,1913 'clean':859,1719 'clear':4147,4761,4975 'code':10,19,36,1002,1403,1491,3987,4009,4035,4409,4414,4477,4485,4576,4751,4753,4763,4959 'codebas':54,4903,4941 'collect':2575,3384 'common':173,177,3556 'common-pitfal':176 'compil':4296,4319,4919 'complet':1607,1632,1634,3488 'complex':703,1893,4446,4782,4948 'compos':249,497,1501 'composit':860,1720,2220,3843,4714 'comprehens':4,27 'concat':2725,2735 'condit':3101,3216 'config':1039,1052,1053,1089,1124,1125,1177 'config.database':1057,1129 'config.database.credentials':1071 'config.database.credentials.password':1074,1077 'config.database.credentials.username':1073,1076 'config.database.host':1061,1080 'config.database.port':1066 'configur':1100,1183 'connecttodatabas':1093,1185 'console.error':364,529,736,901,1096,1179,2305,2380,3089,3923 'console.log':536,906,2300,2383 'consolelogg':2028 'const':301,352,356,414,439,460,505,636,650,675,721,726,773,818,865,889,892,942,963,1064,1086,1122,1137,1141,1223,1251,1291,1303,1317,1441,1457,1462,1575,1676,1697,1721,1748,1768,1809,1846,1977,1996,2025,2029,2034,2039,2129,2138,2152,2166,2181,2195,2221,2260,2263,2294,2362,2365,2368,2395,2398,2401,2404,2469,2472,2533,2538,2548,2633,2637,2644,2657,2674,2717,2744,2791,2795,2800,2847,2871,2910,2920,2971,2983,2999,3027,3173,3219,3275,3299,3358,3372,3393,3419,3490,3514,3536,3570,3575,3590,3617,3623,3649,3676,3705,3727,3747,3781,3814,3820,3824,3845,3861,3889,3894,3913,3967,3983,3997,4043,4071,4094,4120,4190,4198,4217,4243,4305,4353,4384,4449,4514,4560,4613,4639,4649,4676,4685,4739,4771 'constructor':1951 'contain':1890 'content':89 'context':3724,3746 'continu':2558 'control':238 'convers':4927 'convert':63,91,99,105,112,118,124,129,138,145,153,200,580,592,982,1359,1404,1480,1789,1808,1861,2492,3941,4028,4557,4898 'converting-callbacks-to-task':123 'converting-class-based-di-to-read':137 'converting-imperative-loops-to-functional-oper':152 'converting-null-checks-to-opt':111 'converting-try-catch-to-eithertaskeith':98 'correct':3586,3644,3700,3743,3835,3908 'count':2564 'coupl':1876 'creat':556,913,4022,4469,4511,4767,4786,4934 'createappenv':2295,2367 'createtestenv':2396,2471 'createus':4774,4792 'cred':1147 'credenti':1045 'creds.password':1157 'creds.username':1153 'criteria':5011 'critic':4463,4484 'cursor':2899,2914,2925,2932,2974,2985,2995 'data':284,289,291,296,303,353,359,441,447,449,455,462,651,656,814,817,1534,1544,1553,1569,1577,1701,1709,1734,1897,3080,3082,3094,3208,3211,3574,3731,3751,3785,4192 'data.touppercase':1735 'databas':1040,1097,1180,3952 'dataset':2870 'db':1131 'db.credentials':1145 'db.host':1134 'db.port':1139 'dbconnect':2033 'defaultus':3928 'defaultvalu':4840 'defin':2083 'demonstr':4333 'dep':4871 'depend':1881,1888 'describ':2459,4338,4340,4764,4783,4979 'destin':4892 'determin':550 'develop':4263,4632 'di':68,133,141,1865,1874,2023 'difficult':1499 'distinct':1011 'document':4329,4932 'doesn':231,4416,4602,4699,4734 'dosometh':4725 'dosomethingt':4742 'doubl':3596 'driven':4262,4916 'e':380,425,430,568,571,756,925,951,952,955,959,972,973,976,980,1420,1443,1446,1450,1688,1689,1692,1696,1710,1711,1714,1718,2320,2324,2339,2343,2354,2358,3160,3185,3190,3196,3201,3492,3497,3500,3516,3520,3524,3527,3538,3543,3546,3978,3985,4045,4049,4055,4057,4060,4064,4080,4081,4084,4088,4103,4104,4107,4111,4122,4126,4132,4134,4727,4729,4731 'e.either':418,443,509,946,1449,1466,3678,3680,3686,3692,3707,3898,4048,4075,4194 'e.flatmap':515,615,3713 'e.fromoption':1454,1472 'e.getorelse':3921 'e.getorelsew':4054,4131 'e.isleft':3630,4229,4390,4395,4566,4744 'e.isright':2482,4003,4361,4366 'e.left':452,472,483,585,4828 'e.map':3689 'e.mapleft':424 'e.match':527,899,2378 'e.right':492,596,3904,4832 'e.trycatch':949,4078,4823 'easi':225,1020,2389 'ecosystem':3616,3648 'edg':3945 'effect':2490 'effici':2697 'either':261,376,411,433,567,1408,1435,3628,3675,3699,4039,4047,4053,4070,4733 'either/taskeither':96,205 'eithertaskeith':103 'els':1095,1550,1587,1837,4013,4573 'email':1200,1973,2007,2010,2017,2234,2266,2275,2278,2286,2332,2434,2464 'emailservic':1940,1962,1963,2035,2045,2114,2344,2446 'employe':1242,1321 'employee.managerid':1248,1255,1327 'empti':2720 'end':3031,3039 'enrich':4650,4657 'enrichedus':4204 'enrichord':4625,4652 'enrichus':4199,4227,4257 'enrichuserprofil':3085,3214 'ensur':4320,4945 'entir':1885,4902 'env':2149,2163,2178,2192,2210,2366,2375,2470,2480 'env.emailservice.send':2211 'env.logger.error':2164 'env.logger.log':2150 'env.userrepo.findbyid':2179 'env.userrepo.save':2193 'environ':2128,2293,2394,4991 'environment-specif':4990 'environment/dependencies':2085 'equival':73,4820 'err':1543,1546,1548,1568,1571,1573,1621,1626,1630 'error':220,275,278,281,295,314,324,336,338,363,369,412,419,427,435,444,454,474,485,502,510,528,548,552,558,574,583,587,646,648,658,661,666,686,688,694,697,702,735,737,778,787,789,798,800,808,811,823,833,835,844,846,854,857,863,870,900,947,954,957,968,975,978,1028,1437,1467,1474,1495,1531,1532,1561,1562,1601,1602,1681,1691,1694,1704,1713,1716,1728,1757,1777,1813,1814,1822,1828,1831,1836,1851,1923,1991,2096,2105,2112,2123,2133,2144,2158,2172,2187,2205,2229,2255,2303,2306,2322,2341,2356,2379,2382,2420,2423,2978,2981,2991,3074,3088,3095,3097,3117,3136,3138,3142,3178,3187,3189,3198,3200,3204,3224,3234,3251,3255,3259,3285,3357,3363,3377,3430,3431,3441,3443,3573,3634,3654,3679,3681,3687,3693,3708,3718,3723,3730,3737,3742,3745,3750,3758,3766,3888,3899,3922,3926,3981,4016,4018,4042,4059,4062,4068,4076,4083,4086,4099,4106,4109,4203,4379,4571,4618,4647,4697,4713,4720,4827 'error.message':534,903,3143,3260 'everyth':4908 'exampl':86,1456 'execut':885,3352,3371,3569,3587 'exist':33,51,2431,2477,4008 'existing-us':2430,2476 'expect':2481,4335,4360,4368,4389,4397,4537,4777,4797 'expens':2576 'expensiveproduct':2528,2539,2586,2675,2688,2715,2724,2741,2762 'expensiveproducts.push':2581 'expert':4996 'explicit':436,520,1170,3218 'export':4189,4197,4210,4242 'express':4541,4545 'extern':3963,4160,4172 'externalapiclient.getuser':3976 'f':944,950,965,971,1811,1827,4073,4079,4458,4459 'fail':365,530,662,698,902,2381,3090,3353,3418,3738,3761,3925,4803,4806 'failur':3387,3426,3452,3456,3509 'fallback':3522,3532,3535 'fallibl':251 'fals':1611,3141,3258 'familiar':4589,4628 'fast':3354 'fetch':639,664,678,700,770,782,827,3065,3092,3182,3734,3754,3788 'fetchallus':3270,3359 'fetchalluserssequenti':3373 'fetchdata':3571,3577,3593,3642,3661,3728,3748,3782 'fetcherror':3768,3784,3791,3800 'fetchpag':2898,2924,2973,2994 'fetchus':631,724,774,879,3279,3307,3369,3383,3407,3438,3690,3714,4221,4251 'fetchuserapi':3968,4000 'fetchuserdata':3060,3174 'fetchuserpost':669,729,819,883 'fetchuserspartit':3420 'fetchuserswithfallback':3292,3394 'file':1594,1743,1750,1760,1770,1780,3955,4187,4205,4240 'files.foreach':1612 'files.length':1635 'find':1189,1346 'findbyid':1929,2101,2309,2426 'findus':1458,1470,2167,2239 'finduserbyid':1203,1225,1253,1292,1311,1330 'fine':4426,4675 'first':3356,3486,3966,4279,4432,4438,4451,4457 'flatmap':501,2770,3667,3702 'flatten':3704 'flow':239,1898 'foldmap':2695 'forc':4594 'forget':227,1022,3560 'formatnam':4431,4450 'found':1478,1988,1995,2251,2259 'fp':2,13,22,39,57,84,374,383,391,398,742,751,759,766,928,936,1103,1112,1119,1264,1273,1281,1288,1402,1415,1423,1430,1640,1654,1662,1669,1800,2048,2057,2065,2073,2080,2589,2599,2607,2614,2622,2630,2704,2813,2821,2828,2836,2844,2938,2946,2954,2961,3024,3146,3155,3163,3170,3317,3325,3333,3341,3348,3467,3475,3482,3604,3614,4031,4152,4185,4343,4421,4466,4581,4592,4606,4812,4818,4885 'fp-refactor':1 'fp-ts':12,21,38,56,83,373,382,390,397,741,750,758,765,927,935,1102,1111,1118,1263,1272,1280,1287,1414,1422,1429,1639,1653,1661,1668,1799,2047,2056,2064,2072,2079,2588,2598,2606,2613,2621,2629,2703,2812,2820,2827,2835,2843,2937,2945,2953,2960,3023,3145,3154,3162,3169,3316,3324,3332,3340,3347,3466,3474,3481,3603,3613,4030,4151,4184,4342,4420,4465,4591,4605,4811,4817,4884 'fromcallback':1810,1853 'fromnul':1358,3807 'frompromis':4095 'fs':1523,1525,1646 'fs.promises':1673 'fs.readfile':1539,1684,1855 'fs.writefile':1579,1707 'fs/promises':1648 'function':15,41,72,149,157,235,265,282,345,630,668,710,918,941,962,1050,1202,1215,1238,1526,1554,1592,2135,2217,2496,2518,2591,2786,2896,3059,3102,3269,3291,3942,3993,4024,4157,4182,4212,4430,4490,4634,4759,4900,4912,4961 'generat':3017 'generic':1803 'get':1236,3697 'getallproductid':2787,2848 'getallproductidsset':2872 'getdatabaseurl':1051,1088,1123,1176 'getmanageremail':1239,1318 'getmanagernam':3862 'getnam':3815,3846 'getord':3108,3227 'getus':1463,4213,4244,4306,4341,4356,4387,4563 'getuseremail':1216,1304 'getuserwithpost':711,866,895 'go':2984,3008,3012,4705 'goal':4952 'gradual':180,185,3930 'gradual-adoption-strategi':184 'guard':1009 'guid':5,544,1341,4270,4920,4938 'guidanc':77 'handl':221,517,887,1389,1496,1508,3286,3880,3910,4322,4721 'handler':3951 'handleuserrequest':3994 'hard':1492,1894 'harder':245,4610 'haserror':1610,1616,1623,1627 'hell':1489,1591 'help':996 'helper':910,2125,4026 'hierarchi':1887 'host':1041,1136,1166 'hot':4473 'http':647,687,799,845,3075,3205 'hurt':4597 'id':632,641,712,725,730,775,784,867,880,884,1196,1206,1214,1219,1227,1295,1301,1307,1313,1459,1464,1471,1476,1930,2102,2168,2180,2310,2317,2319,2335,2427,2429,2433,2508,2776,2884,2887,3271,3278,3280,3293,3303,3308,3360,3366,3374,3380,3395,3402,3405,3408,3421,3428,3433,3436,3439,3442,3969,3977,4214,4222,4245,4252,4282,4289,4308,4359,4373,4382,4550 'identifi':546,1343 'ids.map':3277,3301 'ignor':3386,3886 'imper':8,18,34,52,146,154,263,627,1036,1192,1519,1908,2493,2504,2772,2894,3057,3266,3990,4034,4476,4555,4809,4815 'implement':4309 'implicit':223 'import':378,386,394,746,754,762,923,931,1107,1115,1268,1276,1284,1410,1418,1426,1521,1644,1649,1657,1665,1795,2052,2060,2068,2076,2594,2602,2610,2617,2625,2698,2816,2824,2831,2839,2941,2949,2957,3019,3150,3158,3166,3320,3328,3336,3344,3462,3470,3478 'inconsist':1498 'incorrect':3606 'individu':3288,4899 'info':2301,2418 'inform':3719 'initi':3991,4163 'inject':1889 'input':267,273,347,355,416,423,507,514,1596,1613,1619,1752,1763,1766,1772,1783,1786,3620,3626,3651,3658,3685,3712,3891,3897,3915,3920,3959,5005 'inputpath':1556,1567,1723,1732 'instanceof':953,974,1690,1712,4058,4082,4105 'instead':1384,3390 'instock':2516 'instockproduct':2638,2647,2660,2677 'interfac':401,1038,1194,1398,1917,1927,1939,2088,2089,2506,2707,2774,2780,2964,4531,4547 'intermedi':4470,4512 'intern':3986,4156,4175,4181 'invalid':279,428 'invoc':3597 'involv':62 'isn':4587 'isol':82 'issu':219 'item':2778,2801,2858,2882,2903,2921,2927,2966,2997,3002,3289,4489 'item.productid':2806,2808,2859,2883 'j':388 'j.parse':422 'journey':4889 'json':280,413,429,809,855,4570 'json.parse':272,408 'keep':3414,4159,4475,4479,4554 'key':4893 'know':4604 'larg':2869 'last':4434,4439,4453,4460 'lead':1025 'left':1834,1835,3451,3882,4119,4376 'let':1068,1606,1609,2530,2542,2913,4495,4499,4917 'librari':4530,4535 'like':3510 'limit':4967 'linear':243 'log':1919,2092,2298,2399,2415 'logerror':2153,2247,3137,3254 'logger':1918,1954,1955,2026,2043,2091,2297,2414 'logger.error':4728,4746 'loginfo':2139,2232,2285 'logs.push':2417,2422 'longer':607 'lookup':1235 'loop':70,147,155,2494,2500,2768,2890,4464 'lose':3717 'lost':3726,3739 'm':2701,2719,3875 'm.name':3876 'main':890,2363 'maintain':4612,4955 'make':434,1490 'manag':1252,1257,1334 'manager.email':1261,1335 'managerid':1243,1322,1329,1332 'manual':2022 'map':3665,4814 'map/filter/reduce':2502 'match':4544,4976 'matter':4701 'member':4583 'messag':1920,1924,2093,2097,2140,2151,2154,2165,3779 'messi':1237 'middlewar':4542 'might':920 'migrat':32,76,160,167,3046,4141,4272,4882 'migrating-promise-chains-to-taskeith':166 'million':4487 'miss':1013,5013 'mix':3600 'mock':1884,2393 'modul':4138,4140,4145 'module-by-modul':4137 'monoid':2627,2699 'move':4168 'msg':2299,2302,2304,2307,2416,2419,2421,2424 'much':4737 'multipl':250,1742,3859 'must':297,326,456,487,4543 'n':2619,3037,3038,3041,3042,4524,4526 'name':315,331,403,475,493,523,1198,2436,2510,3825,3833,3852,4677,4679,4686,4775,4793 'natur':1174 'necessarili':4758 'need':75,608,3669,3841,4303,4712,4909 'nest':341,2767,3674,3698 'network':788,834,3188,3759 'networkerror':3770,3793 'new':277,294,313,323,426,453,473,484,582,586,645,660,685,696,786,797,807,832,843,853,956,977,1473,1693,1715,1824,1990,2027,2031,2036,2041,2254,2321,2340,2355,2885,3073,3116,3186,3197,3203,3233,3633,3736,3757,4015,4061,4085,4108,4646,4826,4869 'new@email.com':2374,2479 'newaccumul':3000,3007,3010 'newemail':1968,2000,2015,2225,2267,2283 'newgetus':4288,4307 'nextcursor':2905,2922,2929,2933,2968,2998,3004,3009 'node':1513 'node-styl':1512 'non':242,1401,4580 'non-fp':1400,4579 'non-linear':241 'none':1439 'note':3594 'notfound':4378,4401 'notif':2467 'null':65,106,113,350,371,719,739,983,994,998,1008,1017,1023,1031,1055,1059,1063,1084,1092,1222,1231,1246,1250,1259,1349,1373,1378,1533,1536,1549,1552,1563,1589,1603,1637,1815,1818,1934,2107,2174,2438,2901,2907,2916,2917,2930,2970,2976,2987,3005,3013,3298,3311,3400,3410,3818,3849,3865,4286,4659,4670,4836 'null/undefined':990 'nullabl':1344,1360 'number':321,329,406,481,490,1044,2513,2523,2527,2537,2665,2710,2714,2785,3030,3032,3034,4492,4493,4494,4506,4516,4517,4518,4520 'numbers.length':4503 'o':1109,1270,1412 'o.bind':1150,1154 'o.do':1149 'o.filter':1383 'o.flatmap':1130,1146,1328,1382,3870,4845 'o.fromnullable':1128,1133,1144,1152,1156,1326,3822,3868,3872,4688,4837 'o.getorelse':1163,1393,4693,4841 'o.isnone':4838 'o.map':1135,1158,1314,1333,1381,3828,3874,4690,4844 'o.match':1178,1394 'o.none':3044 'o.option':1126,1297,1309,1324,1448,1461,3866 'o.some':3040 'o.tonullable':1396,3831 'obj':302,461 'obj.age':320,334,480,496 'obj.name':310,332,469,494 'object':292,300,450,459 'occur':554 'old@email.com':2435 'oldemail':1997,2006,2261,2274 'oldgetus':4281 'one':4315,4317,4668 'one-off':4667 'onnon':1445,1455 'oper':150,158,252,618,1190,1510,2497,2592,3860,3957,4468 'option':109,116,986,1034,1105,1173,1266,1353,1363,1375,1406,1433,1447,1453,3837,3856,4664 'optiontoeith':1442 'optus':3821,3827 'orchestr':704 'order':2775,2788,2789,2796,2798,2849,2850,2853,2855,2873,2874,2877,2879,3111,3118,3122,3124,3126,3230,3235,3248,4615,4616,4622,4636,4637,4642 'order.items':2803,2856,2880 'order.status':3113,3231 'orderid':3104,3109,3221,3228 'orderitem':2779,2781 'orderresult':3225 'origin':3722,3741 'output':1598,1614,1620,1754,1764,1767,1774,1784,1787,4985 'outputpath':1558,1580,1725,1739 'overkil':4681 'overus':3806 'p':2642,2649,2679,2683,2752,4096,4102 'p.instock':2643,2753 'p.name':2684 'p.price':2650,2680 'page':2965,2979 'pagin':2897,2972 'paidord':3132,3134 'parallel':1745,3351 'pars':810,856,3199,3624,3631,3924 'parsed.left.message':3635 'parsed.right':3643 'parseerror':3774,3802 'parseinput':3625,3657 'parsejson':266,354,415,513 'parseus':3896,3919 'parseuserid':3684,3711 'parti':4529 'pass':2693 'password':1048,1155,1160,1162 'path':1528,1540,1678,1685,1699,1708,1848,1856,4474 'pattern':16,28,43,59,255,619,1029,1187,1511,1788,1902,2498,2766,2888,3051,3261,3457,4539,4816,4933 'perform':2867,4462,4483 'performance-crit':4461,4482 'permiss':5006 'phase':4179,4235 'pick':4143 'pipe':395,421,499,512,521,613,763,780,825,875,897,1116,1127,1132,1143,1148,1175,1285,1310,1325,1427,1452,1469,1666,1730,1759,1779,2077,2146,2160,2175,2189,2207,2231,2246,2269,2376,2611,2639,2646,2659,2676,2732,2749,2825,2852,2876,2958,2993,3055,3167,3180,3226,3252,3345,3365,3379,3401,3406,3432,3437,3479,3529,3551,3656,3683,3710,3732,3752,3786,3826,3867,3918,3974,4052,4220,4250,4456,4519,4620,4687,4791 'pitfal':174,178,3557,3558,3598,3662,3715,3804,3877 'port':1043,1065,1081,1138,1167 'post':673,701,717,718,727,733,824,873,874,882,905,909 'postgresclient.query':2312,2328 'postgresuserrepositori':2032 'potenti':3887 'pragmat':4906 'present':1015 'preserv':3744 'price':2512,2654,2656 'primari':3518,3530 'principl':4894 'privat':1952,1956,1960 'problem':207,988,1485,1869 'process':367,532,1576,1581,1737,1740,1741,4005,4486 'processdata':3618,3650 'processfil':1555,1618,1722,1765,1785 'processmultiplefil':1593 'processmultiplefilesparallel':1749 'processmultiplefilessequenti':1769 'processord':3103,3220,4614,4635 'processpay':3129,3241 'processproduct':2519,2634 'processproductssinglepass':2745 'processus':3890,3914,4011 'processuserinput':346,506,522 'product':2507,2520,2521,2549,2550,2577,2635,2636,2640,2667,2746,2747,2750,2756,4598 'product.category':2568,2571,2574,2669,2671,2760 'product.instock':2557 'product.name':2582,2765 'product.price':2563,2579,2758,2763 'productid':2782,2792,2810 'productids.includes':2805 'productids.push':2807 'products.length':2546 'productstat':2708,2748 'productstatsmonoid':2718,2755 'program':42,2361,4962 'promis':161,168,634,672,714,966,1825,1932,1938,1948,1970,2902,2908,3047,3063,3099,3106,3273,3276,3283,3295,3300,3314,3622,4091,4097,4116,4128,4216,4284,4638 'promise.all':3262,3282,3313,4863 'promise.any':3511 'promise.catch':4860 'promise.race':3458,3502 'promise.then':3052,4857 'propag':503,864 'properti':1004,1354,4843 'provid':26,1436,4949 'public':4207,4238 'quantiti':2784 'queri':3953 'quick':4807 'r':2054,2604 'r.asks':4872 'r.union':2734 'ra':3021 'ra.unfold':3035 'race':3485 'racetaskeith':3491 'rang':3028 're':605,4704 'read':1494 'readabl':4756 'reader':135,143,1867,1906,2050,2219,2387 'readertaskeith':2137 'readfil':1677,1731 'readfilecallback':1527,1566 'readfilelegaci':1847 'readon':1953,1957,1961,3033 'reason':247,785,790,806,812,831,836,852,858,3756,3762 'record':305,464,2525,2535,2663,2712 'recurs':4879 'recursion/unfold':2892 'refactor':3,7,17,49,192,198,543,1340,4406,4412,4922 'refer':4808 'remov':599 'replac':577,589,1377 'req':4552 'req.params.id':4564 'request':3760 'requir':317,477,1006,1083,1883,5004 'res':4553 'res.json':4574 'res.status':4568 'resolv':1826,1832,1838 'respons':637,676,792,795,803,838,841,849,3069,3192,3796,3950 'response.json':653,692,805,851,3078,3195,3798 'response.ok':643,683,793,839,3071,3193 'response.status':649,689,801,847,3076,3206 'result':888,893,898,1816,1829,1842,2369,2377,2473,2483,3576,3585,3591,3655,3672,3677,3706,3895,3902,3954,3998,4004,4218,4230,4354,4362,4367,4385,4391,4396,4561,4567,4619,4740,4745 'result.left':4232,4398,4747,4749 'result.left.message':4572 'result.left.type':4019 'result.right':4012,4234,4575 'result.right.id':4369 'return':271,330,360,370,410,432,451,471,482,491,562,590,593,654,690,731,738,1058,1062,1078,1164,1210,1230,1232,1249,1258,1260,1369,1574,1617,1624,1631,2245,2268,2413,2443,2455,2583,2685,2809,2934,3003,3011,3064,3077,3107,3121,3139,3281,3305,3310,3312,3627,3640,3691,3832,3901,3927,4010,4233,4346,4375,4437,4508,4654,4830 'reusabl':915 'review':4943,4997 'right':1840,1841,3453,3905,4347 'rte':2062 'rte.ask':2131,4873 'rte.flatmap':2238,2241,2252,2272,2284 'rte.flatmaptaskeither':2177,2191,2209 'rte.left':2253 'rte.map':2148,2162 'rte.readertaskeither':2142,2156,2170,2185,2203,2227 'run':2359,3562 'runtim':1027,1892 'safe':4958 'safeti':5007 'sake':4966 'save':1935,2108,2325,2439 'savedus':2402 'savedusers.push':2441 'saveus':2182,2270 'scope':4978 'select':2313 'send':1941,2115,2345,2447,2466 'sendemail':2196,2273 'sentemail':2405 'sentemails.push':2451 'sequenc':1503,1747,3018 'sequenti':3370 'servic':1903,2134 'set':2331,2864,2875,2886 'setup':2024 'sever':218 'ship':3245,3249 'shipord':3133,3243 'show':4298 'side':2489 'signatur':4278 'simpl':1030,4407,4666,4683 'singl':2692 'site':4301,4314 'skill':25,4970 'skill-fp-refactor' 'skip':2552 'small':4897 'smtpclient.send':2350 'smtpconfig':2038 'smtpemailservic':2037 'source-sickn33' 'specif':4538,4937,4992 'spread':1000 'stabl':4177 'standard':1505 'start':3029,3036,3935,4896 'statement':579,591,1387 'stay':3645,3989 'step':540,542,1337,1339,4274,4294,4310 'step-by-step':539,1336 'still':999,3580 'stock':2555 'stop':4998 'str':2841 'str.eq':2861 'straightforward':4413 'strategi':30,182,187,3932,3933,4020,4135,4258,4325 'strict':993 'string':268,306,311,348,404,417,465,470,508,633,671,713,776,821,868,958,979,1042,1047,1049,1054,1197,1199,1201,1207,1220,1221,1244,1245,1296,1308,1323,1460,1465,1529,1535,1557,1559,1597,1599,1679,1682,1695,1700,1702,1717,1724,1726,1753,1755,1773,1775,1849,1852,1921,1925,1931,1943,1945,1947,1967,1969,2094,2098,2103,2117,2119,2121,2141,2155,2169,2198,2200,2202,2224,2226,2323,2342,2357,2400,2408,2410,2412,2509,2511,2515,2526,2529,2536,2540,2664,2713,2716,2777,2783,2790,2793,2851,2900,2906,2915,2969,2975,2986,3062,3105,3176,3222,3272,3294,3361,3375,3396,3422,3429,3621,3652,3688,3780,3819,3850,3892,3916,3970,3996,4063,4087,4110,4215,4246,4283,4290,4433,4435,4436,4452,4454,4455 'style':1514 'subject':1944,2118,2199,2213,2347,2352,2409,2449,2453 'submitord':4627,4656 'substitut':4988 'success':907,2384,3140,3246,3257,3385,3424,3454,3455,4351,5010 'sum':2559,4496,4505,4509 'sumlargearray':4491 'summari':4881 'sumwithfpt':4515 'sync':940 'synchron':256,4408 'system':230,1365,3948,3956,4268 't.applicativepar':3404,3435 't.map':3448,3450 't.of':4802 't.task':3397,3423 'tabl':87 'tag':1833,1839,3769,3773,3777,3792,3801,4399 'task':61,121,127,1483,1517,3389,3494,3540,3550,3554,3563,3566,3582,3589,4974 'task/taskeither':1642 'taskeith':164,171,625,744,772,1675,1807,3050,3148,3392,4093,4114 'tasks.map':3503 'tasks.reduce':3548 'te':748,933,1651,1797,2070,2943,3152,3322,3446,3447,3464,3504,3505,4124,4129 'te.applicativepar':1762,3368,4865 'te.applicativeseq':1782,3382 'te.bimap':3440 'te.bind':877,881 'te.do':876 'te.filterorelse':3229 'te.flatmap':791,802,813,837,848,1736,2996,3191,3207,3212,3238,3240,3242,3660,3795,4223,4226,4253,4256,4624,4626,4859,4867 'te.fromeither':815,3209,3659,4225,4255,4623 'te.fromio':3253 'te.getorelse':4801 'te.left':796,842,3202,4829 'te.map':1733,3244,3256,4795,4858 'te.mapleft':4862 'te.match':3409 'te.orelse':3250,3531,3553,4861 'te.right':794,840,2428,2444,2456,3006,4833 'te.taskeither':777,822,869,967,1680,1703,1727,1756,1776,1821,1850,2104,2111,2122,2977,2980,2990,3177,3223,3362,3376,3496,3499,3519,3523,3526,3542,3545,3572,3653,3729,3749,3783,3971,4098,4125,4202,4247,4291,4617 'te.trycatch':781,804,826,850,970,1683,1706,2311,2327,2349,3181,3194,3733,3753,3787,3797,3975,4101,4824 'team':4582,4586,4601,4936 'team-specif':4935 'test':1882,2385,2391,2437,2458,4165,4327,4331,4750,4752,4762,4924,4994 'third':4528 'third-parti':4527 'this.emailservice.send':2005 'this.logger.error':1984 'this.logger.log':1971,2016 'this.userrepo.findbyid':1980 'this.userrepo.save':2002 'thorough':4166,4925 'throw':237,276,293,312,322,337,578,581,644,659,684,695,921,1989,3072,3096,3115,3632,4014,4056,4117,4133,4231,4645,4730,4748,4825 'throw/log':4707 'thrown':4041,4067 'tight':1875 'tobe':2484,4363,4370,4392,4400,4779,4799 'topic-agent-skills' 'topic-agentic-skills' 'topic-ai-agent-skills' 'topic-ai-agents' 'topic-ai-coding' 'topic-ai-workflows' 'topic-antigravity' 'topic-antigravity-skills' 'topic-claude-code' 'topic-claude-code-skills' 'topic-codex-cli' 'topic-codex-skills' 'topromis':4121 'total':2560 'totalvalu':2522,2531,2562,2584,2645,2686,2709,2721,2728,2757 'touch':4577 'touppercas':1578 'toward':55 'trace':1896 'track':233,3415 'tradeoff':79 'tradit':212,1912 'travers':3264 'treat':4983 'tri':93,100,202,210,214,258,270,287,343,351,601,622,635,674,707,720,3304,3506,4723,4821 'trivial':4658 'true':1628,2485,2919,3247,4364,4393 'try-catch':92,201,209,213,257,342,600,621,706 'try/catch':64 'tryaltern':3515 'trycatch':911 'trycatchasync':964 'trycatchsync':943 'ts':14,23,40,58,85,375,384,392,399,743,752,760,767,929,937,1104,1113,1120,1265,1274,1282,1289,1416,1424,1431,1641,1655,1663,1670,1801,2049,2058,2066,2074,2081,2590,2600,2608,2615,2623,2631,2705,2814,2822,2829,2837,2845,2939,2947,2955,2962,3025,3147,3156,3164,3171,3318,3326,3334,3342,3349,3468,3476,3483,3605,3615,4032,4153,4186,4344,4422,4467,4593,4607,4813,4819,4886 'type':229,438,549,559,563,575,1370,3513,3765,3767,3979,4154,4261,4267,4277,4280,4287,4698,4915,4957 'type-driven':4260,4914 'type-saf':4956 'typeof':290,309,319,448,468,479 'typescript':9,35,53,264,377,628,745,922,991,1037,1106,1193,1267,1409,1520,1643,1794,1911,2051,2388,2505,2593,2773,2815,2895,2940,3058,3149,3267,3319,3461,3564,3607,3670,3720,3808,3884,3961,4036,4178,4265,4273,4337,4423,4478,4540,4599,4631,4672,4715,4760 'u':1212,1299,3829,3871,4691 'u.id':1213,1300 'u.manager':3873 'u.name':3830,4692 'unchang':4162,4209 'unclear':1019 'undefin':1209,1351,2445,2457,2569 'unfold':3015,4880 'unknown':269,285,307,420,442,466,3772,3776,3834,3853,4193,4381,4388 'unnecessari':3810 'unnecessarili':4781 'unsaf':3906 'unsafeunwrap':4044 'unwrap':3813 'updat':610,1972,2018,2233,2287,2329,2463,4171,4237,4304,4312 'updatedus':2264,2271 'updateemail':1965,2216,2222,2371,2460,2475 'url':1087,1091,1094,1098,1181,1184,1186 'usag':339,1082,1168,1845 'use':46,407,612,1380,1392,2136,2218,2690,2863,3014,3388,3664,3701,3764,3836,3855,4183,4264,4663,4968 'user':286,349,357,361,368,402,445,511,533,535,537,538,665,715,716,722,732,779,871,872,878,904,908,1195,1204,1205,1208,1217,1218,1224,1226,1229,1240,1241,1254,1293,1294,1302,1305,1306,1312,1315,1319,1320,1331,1468,1475,1933,1936,1937,1975,1978,1983,1985,1992,2003,2020,2106,2109,2110,2173,2183,2184,2194,2236,2242,2244,2248,2256,2265,2289,2315,2326,2330,2372,2403,2432,2440,2442,2478,3093,3274,3297,3364,3378,3399,3411,3412,3425,3444,3445,3682,3694,3709,3816,3817,3823,3847,3848,3851,3863,3864,3869,3893,3900,3917,3958,4006,4196,4200,4201,4249,4285,4293,4349,4678,4689,4769,4772,4788,4796 'user-service.internal.ts':4188 'user-service.ts':4206,4241 'user.email':1233,1316,1998,1999,2262,2337 'user.id':2338 'user.name':4778,4798 'userdto':3973 'usererror':4248,4292 'userid':670,680,820,829,1966,1976,1981,1986,1993,2021,2223,2237,2240,2249,2257,2290,3061,3067,3175,3184,3995,4001 'usernam':1046,1151,1159,1161 'userprofil':3179 'userrepo':1958,2030,2044,2100,2308,2425 'userrepositori':1928,1959 'users.find':1211 'userservic':1950,2040,2042,4339,4765,4784 'utf':1541,1686,1857 'util':912 'valid':431,3960,4358,4372,4640,4993 'valid-id':4357,4371 'validated.data':4653 'validated.error':4648 'validated.success':4644 'validateinventori':3125,3239 'validateord':4621,4641 'validateus':283,358,440,516,655,816,4191,4224,4254 'validateuserdata':3081,3210 'validationerror':3778,4195 'validdata':3084,3086,3213,3215 'validord':3128,3130 'valu':594,597,1345,1361,2561,4738,4831,4950 'verbos':254,1007 'verifi':4930 'void':1537,1538,1564,1565,1604,1605,1705,1729,1758,1778,1819,1820,1922,1926,2095,2099,2113,2124,2145,2159,2188,2206,2230 'way':1506 'when-not-to-refactor':194 'win':3489 'withfallback':3537 'without':4447 'work':4533 'wrap':769,1356,1672,3811,3962 'wrapper':916 'write':4330 'writeerr':1582,1584,1586 'writefil':1698,1738 'wrong':3565,3608,3671,3721,3809,3885 'x':2736,2738,4835,4839,4842 'y':2737,2739","prices":[{"id":"409da4a6-0edf-405e-bc60-bca4369cdca3","listingId":"03e6e53e-9e4f-4cde-a2d6-a4efa73153f7","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"sickn33","category":"antigravity-awesome-skills","install_from":"skills.sh"},"createdAt":"2026-04-18T21:37:29.740Z"}],"sources":[{"listingId":"03e6e53e-9e4f-4cde-a2d6-a4efa73153f7","source":"github","sourceId":"sickn33/antigravity-awesome-skills/fp-refactor","sourceUrl":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/fp-refactor","isPrimary":false,"firstSeenAt":"2026-04-18T21:37:29.740Z","lastSeenAt":"2026-04-24T00:50:58.479Z"}],"details":{"listingId":"03e6e53e-9e4f-4cde-a2d6-a4efa73153f7","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"sickn33","slug":"fp-refactor","github":{"repo":"sickn33/antigravity-awesome-skills","stars":34793,"topics":["agent-skills","agentic-skills","ai-agent-skills","ai-agents","ai-coding","ai-workflows","antigravity","antigravity-skills","claude-code","claude-code-skills","codex-cli","codex-skills","cursor","cursor-skills","developer-tools","gemini-cli","gemini-skills","kiro","mcp","skill-library"],"license":"mit","html_url":"https://github.com/sickn33/antigravity-awesome-skills","pushed_at":"2026-04-24T00:28:59Z","description":"Installable GitHub library of 1,400+ agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and more. Includes installer CLI, bundles, workflows, and official/community skill collections.","skill_md_sha":"c93a628e992da611dc0b3813d60aa2e2545948c6","skill_md_path":"skills/fp-refactor/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/fp-refactor"},"layout":"multi","source":"github","category":"antigravity-awesome-skills","frontmatter":{"name":"fp-refactor","description":"Comprehensive guide for refactoring imperative TypeScript code to fp-ts functional patterns"},"skills_sh_url":"https://skills.sh/sickn33/antigravity-awesome-skills/fp-refactor"},"updatedAt":"2026-04-24T00:50:58.479Z"}}