[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-8a1956cd-e4f6-4e9c-b6f8-27147a25ee21":3,"$fGeS_yS1YIJQWit3NX2njw-P5MDJjgU_OCILUkj7OsOs":43},{"id":4,"title":5,"description":6,"categoryId":7,"moduleId":8,"tags":9,"prompt":10,"icon":11,"source":12,"sourceUrl":13,"authorId":14,"authorName":15,"isPublic":16,"stars":17,"runs":18,"createdAt":19,"updatedAt":19,"module":20,"category":27,"packages":34},"8a1956cd-e4f6-4e9c-b6f8-27147a25ee21","fp-refactor","全面指南：将命令式TypeScript代码重构为fp-ts函数式模式","cat_life_career","mod_other","sickn33,other","---\nname: fp-refactor\ndescription: Comprehensive guide for refactoring imperative TypeScript code to fp-ts functional patterns\nrisk: unknown\nsource: community\nversion: 1.0.0\nauthor: fp-ts-skills\ntags:\n  - fp-ts\n  - refactoring\n  - functional-programming\n  - typescript\n  - migration\n  - either\n  - option\n  - task\n  - reader\n---\n\n# 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\u002Fcatch`, 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\u002FTaskEither](#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\u002FTaskEither\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\u003Cstring, 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\u002F\u002F 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\u002FEither';\nimport * as J from 'fp-ts\u002FJson';\nimport { pipe } from 'fp-ts\u002Ffunction';\n\ninterface User {\n  name: string;\n  age: number;\n}\n\n\u002F\u002F Use Json.parse which returns Either\u003CError, Json>\nconst parseJSON = (input: string): E.Either\u003CError, unknown> =>\n  pipe(\n    J.parse(input),\n    E.mapLeft((e) => new Error(`Invalid JSON: ${e}`))\n  );\n\n\u002F\u002F Validation returns Either, making errors explicit in types\nconst validateUser = (data: unknown): E.Either\u003CError, 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\u003Cstring, 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\u002F\u002F Compose with pipe and flatMap - errors propagate automatically\nconst processUserInput = (input: string): E.Either\u003CError, User> =>\n  pipe(\n    parseJSON(input),\n    E.flatMap(validateUser)\n  );\n\n\u002F\u002F 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\u003CE, 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\u003CUser> {\n  try {\n    const response = await fetch(`\u002Fapi\u002Fusers\u002F${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\u003CPost[]> {\n  try {\n    const response = await fetch(`\u002Fapi\u002Fusers\u002F${userId}\u002Fposts`);\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\u002F\u002F Complex orchestration with try-catch\nasync function getUserWithPosts(id: string): Promise\u003C{ 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\u002FTaskEither';\nimport * as E from 'fp-ts\u002FEither';\nimport { pipe } from 'fp-ts\u002Ffunction';\n\n\u002F\u002F Wrap fetch in TaskEither\nconst fetchUser = (id: string): TE.TaskEither\u003CError, User> =>\n  pipe(\n    TE.tryCatch(\n      () => fetch(`\u002Fapi\u002Fusers\u002F${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\u003CError, Post[]> =>\n  pipe(\n    TE.tryCatch(\n      () => fetch(`\u002Fapi\u002Fusers\u002F${userId}\u002Fposts`),\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\u002F\u002F Clean composition with automatic error propagation\nconst getUserWithPosts = (\n  id: string\n): TE.TaskEither\u003CError, { user: User; posts: Post[] }> =>\n  pipe(\n    TE.Do,\n    TE.bind('user', () => fetchUser(id)),\n    TE.bind('posts', () => fetchUserPosts(id))\n  );\n\n\u002F\u002F 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\u002FEither';\nimport * as TE from 'fp-ts\u002FTaskEither';\n\n\u002F\u002F For sync functions\nconst tryCatchSync = \u003CA>(f: () => A): E.Either\u003CError, A> =>\n  E.tryCatch(f, (e) => (e instanceof Error ? e : new Error(String(e))));\n\n\u002F\u002F For async functions\nconst tryCatchAsync = \u003CA>(f: () => Promise\u003CA>): TE.TaskEither\u003CError, 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\u002Fundefined\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:\u002F\u002F${auth}${config.database.host}:${port}`;\n}\n\n\u002F\u002F 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\u002FOption';\nimport { pipe } from 'fp-ts\u002Ffunction';\n\nconst getDatabaseUrl = (config: Config): O.Option\u003Cstring> =>\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:\u002F\u002F${auth}${host}:${port}`;\n        })\n      )\n    )\n  );\n\n\u002F\u002F 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\u002F\u002F 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\u002FOption';\nimport * as A from 'fp-ts\u002FArray';\nimport { pipe } from 'fp-ts\u002Ffunction';\n\nconst findUserById = (users: User[], id: string): O.Option\u003CUser> =>\n  A.findFirst\u003CUser>((u) => u.id === id)(users);\n\nconst getUserEmail = (users: User[], id: string): O.Option\u003Cstring> =>\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\u003Cstring> =>\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\u003CT>`\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\u002FOption';\nimport * as E from 'fp-ts\u002FEither';\nimport { pipe } from 'fp-ts\u002Ffunction';\n\n\u002F\u002F Option to Either: provide error for None case\nconst optionToEither = \u003CE, A>(onNone: () => E) => (\n  option: O.Option\u003CA>\n): E.Either\u003CE, A> =>\n  pipe(\n    option,\n    E.fromOption(onNone)\n  );\n\n\u002F\u002F Example\nconst findUser = (id: string): O.Option\u003CUser> => \u002F* ... *\u002F;\n\nconst getUser = (id: string): E.Either\u003CError, 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\u002F\u002F Callback hell\nfunction processMultipleFiles(\n  files: Array\u003C{ 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\u002FTaskEither)\n\n```typescript\nimport * as fs from 'fs\u002Fpromises';\nimport * as TE from 'fp-ts\u002FTaskEither';\nimport * as A from 'fp-ts\u002FArray';\nimport { pipe } from 'fp-ts\u002Ffunction';\n\n\u002F\u002F Wrap fs.promises in TaskEither\nconst readFile = (path: string): TE.TaskEither\u003CError, 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\u003CError, void> =>\n  TE.tryCatch(\n    () => fs.writeFile(path, data),\n    (e) => (e instanceof Error ? e : new Error(String(e)))\n  );\n\n\u002F\u002F Clean composition\nconst processFile = (\n  inputPath: string,\n  outputPath: string\n): TE.TaskEither\u003CError, void> =>\n  pipe(\n    readFile(inputPath),\n    TE.map((data) => data.toUpperCase()),\n    TE.flatMap((processed) => writeFile(outputPath, processed))\n  );\n\n\u002F\u002F Process multiple files in parallel or sequence\nconst processMultipleFilesParallel = (\n  files: Array\u003C{ input: string; output: string }>\n): TE.TaskEither\u003CError, void[]> =>\n  pipe(\n    files,\n    A.traverse(TE.ApplicativePar)(({ input, output }) =>\n      processFile(input, output)\n    )\n  );\n\nconst processMultipleFilesSequential = (\n  files: Array\u003C{ input: string; output: string }>\n): TE.TaskEither\u003CError, 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\u002FTaskEither';\n\n\u002F\u002F Generic callback-to-TaskEither converter\nconst fromCallback = \u003CA>(\n  f: (callback: (error: Error | null, result: A | null) => void) => void\n): TE.TaskEither\u003CError, 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\u002F\u002F Usage\nconst readFileLegacy = (path: string): TE.TaskEither\u003CError, 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\u002F\u002F 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\u003CUser | null>;\n  save(user: User): Promise\u003Cvoid>;\n}\n\ninterface EmailService {\n  send(to: string, subject: string, body: string): Promise\u003Cvoid>;\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\u003Cvoid> {\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\u002F\u002F 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\u002FReader';\nimport * as RTE from 'fp-ts\u002FReaderTaskEither';\nimport * as TE from 'fp-ts\u002FTaskEither';\nimport { pipe } from 'fp-ts\u002Ffunction';\n\n\u002F\u002F Define the environment\u002Fdependencies 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\u003CError, User | null>;\n    save: (user: User) => TE.TaskEither\u003CError, void>;\n  };\n  emailService: {\n    send: (to: string, subject: string, body: string) => TE.TaskEither\u003CError, void>;\n  };\n}\n\n\u002F\u002F Helper to access environment\nconst ask = RTE.ask\u003CAppEnv, Error>();\n\n\u002F\u002F Service functions using ReaderTaskEither\nconst logInfo = (message: string): RTE.ReaderTaskEither\u003CAppEnv, Error, void> =>\n  pipe(\n    ask,\n    RTE.map((env) => env.logger.log(message))\n  );\n\nconst logError = (message: string): RTE.ReaderTaskEither\u003CAppEnv, Error, void> =>\n  pipe(\n    ask,\n    RTE.map((env) => env.logger.error(message))\n  );\n\nconst findUser = (id: string): RTE.ReaderTaskEither\u003CAppEnv, Error, User | null> =>\n  pipe(\n    ask,\n    RTE.flatMapTaskEither((env) => env.userRepo.findById(id))\n  );\n\nconst saveUser = (user: User): RTE.ReaderTaskEither\u003CAppEnv, 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\u003CAppEnv, Error, void> =>\n  pipe(\n    ask,\n    RTE.flatMapTaskEither((env) => env.emailService.send(to, subject, body))\n  );\n\n\u002F\u002F The updateEmail function using Reader composition\nconst updateEmail = (\n  userId: string,\n  newEmail: string\n): RTE.ReaderTaskEither\u003CAppEnv, 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\u002F\u002F 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\u002F\u002F 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\u002F\u002F Easy to test with mock environment\nconst createTestEnv = (): AppEnv => {\n  const logs: string[] = [];\n  const savedUsers: User[] = [];\n  const sentEmails: Array\u003C{ 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\u002F\u002F 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    \u002F\u002F 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\u002Ffilter\u002Freduce\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\u003Cstring, number>;\n  expensiveProducts: string[];\n} {\n  let totalValue = 0;\n  const categoryCounts: Record\u003Cstring, number> = {};\n  const expensiveProducts: string[] = [];\n\n  for (let i = 0; i \u003C products.length; i++) {\n    const product = products[i];\n\n    \u002F\u002F Skip out of stock\n    if (!product.inStock) {\n      continue;\n    }\n\n    \u002F\u002F Sum total value\n    totalValue += product.price;\n\n    \u002F\u002F Count categories\n    if (categoryCounts[product.category] === undefined) {\n      categoryCounts[product.category] = 0;\n    }\n    categoryCounts[product.category]++;\n\n    \u002F\u002F 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\u002FArray';\nimport * as R from 'fp-ts\u002FRecord';\nimport { pipe } from 'fp-ts\u002Ffunction';\nimport * as N from 'fp-ts\u002Fnumber';\nimport * as Monoid from 'fp-ts\u002FMonoid';\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\u003Cstring, 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\u002F\u002F Or using a single pass with foldMap for efficiency\nimport { Monoid as M } from 'fp-ts\u002FMonoid';\n\ninterface ProductStats {\n  totalValue: number;\n  categoryCounts: Record\u003Cstring, number>;\n  expensiveProducts: string[];\n}\n\nconst productStatsMonoid: M\u003CProductStats> = {\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\u002FArray';\nimport { pipe } from 'fp-ts\u002Ffunction';\nimport * as S from 'fp-ts\u002FSet';\nimport * as Str from 'fp-ts\u002Fstring';\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\u002F\u002F Or using Set for better performance with large datasets\nconst getAllProductIdsSet = (orders: Order[]): Set\u003Cstring> =>\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\u002Funfold\n\n#### Before (Imperative)\n\n```typescript\nfunction paginate\u003CT>(\n  fetchPage: (cursor: string | null) => Promise\u003C{ items: T[]; nextCursor: string | null }>\n): Promise\u003CT[]> {\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\u002FTaskEither';\nimport * as A from 'fp-ts\u002FArray';\nimport { pipe } from 'fp-ts\u002Ffunction';\n\ninterface Page\u003CT> {\n  items: T[];\n  nextCursor: string | null;\n}\n\nconst paginate = \u003CT>(\n  fetchPage: (cursor: string | null) => TE.TaskEither\u003CError, Page\u003CT>>\n): TE.TaskEither\u003CError, T[]> => {\n  const go = (\n    cursor: string | null,\n    accumulated: T[]\n  ): TE.TaskEither\u003CError, 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\u002F\u002F Using unfold for generating sequences\nimport * as RA from 'fp-ts\u002FReadonlyArray';\n\nconst range = (start: number, end: number): readonly number[] =>\n  RA.unfold(start, (n) => (n \u003C= 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\u003CUserProfile> {\n  return fetch(`\u002Fapi\u002Fusers\u002F${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\u002F\u002F Chained promises with conditionals\nfunction processOrder(orderId: string): Promise\u003COrderResult> {\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\u002FTaskEither';\nimport * as E from 'fp-ts\u002FEither';\nimport { pipe } from 'fp-ts\u002Ffunction';\n\nconst fetchUserData = (userId: string): TE.TaskEither\u003CError, UserProfile> =>\n  pipe(\n    TE.tryCatch(\n      () => fetch(`\u002Fapi\u002Fusers\u002F${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\u002F\u002F Conditionals are explicit\nconst processOrder = (orderId: string): TE.TaskEither\u003CError, 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\u003CUser[]> {\n  const promises = ids.map((id) => fetchUser(id));\n  return Promise.all(promises);\n}\n\n\u002F\u002F With error handling for individual items\nasync function fetchUsersWithFallback(ids: string[]): Promise\u003CArray\u003CUser | 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\u002FTaskEither';\nimport * as A from 'fp-ts\u002FArray';\nimport * as T from 'fp-ts\u002FTask';\nimport { pipe } from 'fp-ts\u002Ffunction';\n\n\u002F\u002F Parallel execution - fails fast on first error\nconst fetchAllUsers = (ids: string[]): TE.TaskEither\u003CError, User[]> =>\n  pipe(\n    ids,\n    A.traverse(TE.ApplicativePar)(fetchUser)\n  );\n\n\u002F\u002F Sequential execution\nconst fetchAllUsersSequential = (ids: string[]): TE.TaskEither\u003CError, User[]> =>\n  pipe(\n    ids,\n    A.traverse(TE.ApplicativeSeq)(fetchUser)\n  );\n\n\u002F\u002F Collect successes, ignore failures (using Task instead of TaskEither)\nconst fetchUsersWithFallback = (ids: string[]): T.Task\u003CArray\u003CUser | 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\u002F\u002F Or keep track of which failed\nconst fetchUsersPartitioned = (\n  ids: string[]\n): T.Task\u003C{ successes: User[]; failures: Array\u003C{ 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\u002FTaskEither';\nimport * as T from 'fp-ts\u002FTask';\nimport { pipe } from 'fp-ts\u002Ffunction';\n\n\u002F\u002F Race - first to complete wins\nconst raceTaskEithers = \u003CE, A>(\n  tasks: Array\u003CTE.TaskEither\u003CE, A>>\n): TE.TaskEither\u003CE, A> =>\n  () => Promise.race(tasks.map((te) => te()));\n\n\u002F\u002F Try alternatives on failure (like Promise.any but typed)\nconst tryAlternatives = \u003CE, A>(\n  primary: TE.TaskEither\u003CE, A>,\n  fallback: TE.TaskEither\u003CE, A>\n): TE.TaskEither\u003CE, A> =>\n  pipe(\n    primary,\n    TE.orElse(() => fallback)\n  );\n\n\u002F\u002F Chain of fallbacks\nconst withFallbacks = \u003CE, A>(\n  tasks: Array\u003CTE.TaskEither\u003CE, A>>\n): TE.TaskEither\u003CE, 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\u002F\u002F WRONG: Task is not executed\nconst fetchData = (): TE.TaskEither\u003CError, Data> => \u002F* ... *\u002F;\nconst result = fetchData(); \u002F\u002F This is still a Task, not the result!\n\n\u002F\u002F CORRECT: Execute the Task\nconst result = await fetchData()(); \u002F\u002F Note the double invocation\n```\n\n### Pitfall 2: Mixing async\u002Fawait with fp-ts incorrectly\n\n```typescript\n\u002F\u002F WRONG: Breaking out of the fp-ts ecosystem\nconst processData = async (input: string): Promise\u003CResult> => {\n  const parsed = parseInput(input); \u002F\u002F Returns Either\n  if (E.isLeft(parsed)) {\n    throw new Error(parsed.left.message); \u002F\u002F Don't do this!\n  }\n  return await fetchData(parsed.right)();\n};\n\n\u002F\u002F CORRECT: Stay in the ecosystem\nconst processData = (input: string): TE.TaskEither\u003CError, 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\u002F\u002F WRONG: Results in nested Either\nconst result: E.Either\u003CError, E.Either\u003CError, User>> = pipe(\n  parseUserId(input), \u002F\u002F E.Either\u003CError, string>\n  E.map(fetchUser) \u002F\u002F Returns E.Either\u003CError, User>, so we get nested Either\n);\n\n\u002F\u002F CORRECT: Use flatMap to flatten\nconst result: E.Either\u003CError, User> = pipe(\n  parseUserId(input),\n  E.flatMap(fetchUser)\n);\n```\n\n### Pitfall 4: Losing error information\n\n```typescript\n\u002F\u002F WRONG: Original error context is lost\nconst fetchData = (): TE.TaskEither\u003CError, Data> =>\n  pipe(\n    TE.tryCatch(\n      () => fetch('\u002Fapi\u002Fdata'),\n      () => new Error('Failed') \u002F\u002F Lost the original error!\n    )\n  );\n\n\u002F\u002F CORRECT: Preserve error context\nconst fetchData = (): TE.TaskEither\u003CError, Data> =>\n  pipe(\n    TE.tryCatch(\n      () => fetch('\u002Fapi\u002Fdata'),\n      (reason) => new Error(`Network request failed: ${reason}`)\n    )\n  );\n\n\u002F\u002F 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\u003CFetchError, Data> =>\n  pipe(\n    TE.tryCatch(\n      () => fetch('\u002Fapi\u002Fdata'),\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\u002F\u002F 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\u002F\u002F CORRECT: Use Option only when you need its composition benefits\nconst getName = (user: User | null): string => user?.name ?? 'Unknown';\n\n\u002F\u002F BETTER: Use Option when chaining multiple operations\nconst getManagerName = (user: User | null): O.Option\u003Cstring> =>\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\u002F\u002F WRONG: Ignoring potential errors\nconst processUser = (input: string): User => {\n  const result = parseUser(input); \u002F\u002F E.Either\u003CError, User>\n  return (result as E.Right\u003CUser>).right; \u002F\u002F Unsafe cast!\n};\n\n\u002F\u002F 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\u002F\u002F Wrap external API calls first\nconst fetchUserApi = (id: string): TE.TaskEither\u003CApiError, UserDto> =>\n  pipe(\n    TE.tryCatch(\n      () => externalApiClient.getUser(id),\n      (e) => ({ type: 'api_error' as const, cause: e })\n    )\n  );\n\n\u002F\u002F Internal code can stay imperative initially\nasync function handleUserRequest(userId: string) {\n  const result = await fetchUserApi(userId)();\n  if (E.isRight(result)) {\n    \u002F\u002F 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\u002F\u002F Bridge from Either to thrown errors\nconst unsafeUnwrap = \u003CE, A>(either: E.Either\u003CE, A>): A =>\n  pipe(\n    either,\n    E.getOrElseW((e) => {\n      throw e instanceof Error ? e : new Error(String(e));\n    })\n  );\n\n\u002F\u002F Bridge from thrown errors to Either\nconst catchSync = \u003CA>(f: () => A): E.Either\u003CError, A> =>\n  E.tryCatch(f, (e) => (e instanceof Error ? e : new Error(String(e))));\n\n\u002F\u002F Bridge from Promise to TaskEither\nconst fromPromise = \u003CA>(p: Promise\u003CA>): TE.TaskEither\u003CError, A> =>\n  TE.tryCatch(() => p, (e) => (e instanceof Error ? e : new Error(String(e))));\n\n\u002F\u002F Bridge from TaskEither to Promise (throws on Left)\nconst toPromise = \u003CE, A>(te: TE.TaskEither\u003CE, A>): Promise\u003CA> =>\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\u002F\u002F Phase 1: Internal functions use fp-ts\n\u002F\u002F File: user-service.internal.ts\nexport const validateUser = (data: unknown): E.Either\u003CValidationError, User> => \u002F* ... *\u002F;\nexport const enrichUser = (user: User): TE.TaskEither\u003CError, EnrichedUser> => \u002F* ... *\u002F;\n\n\u002F\u002F File: user-service.ts (public API unchanged)\nexport async function getUser(id: string): Promise\u003CUser> {\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\u002F\u002F Phase 2: Update public API\n\u002F\u002F File: user-service.ts\nexport const getUser = (id: string): TE.TaskEither\u003CUserError, 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\u002F\u002F Step 1: Change type signature first\ntype OldGetUser = (id: string) => Promise\u003CUser | null>;\ntype NewGetUser = (id: string) => TE.TaskEither\u003CUserError, User>;\n\n\u002F\u002F Step 2: Compiler will show all call sites that need updating\nconst getUser: NewGetUser = (id) => \u002F* implement *\u002F;\n\n\u002F\u002F Step 3: Update call sites one by one\n\u002F\u002F 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\u002F\u002F This is fine as-is\nfunction formatName(first: string, last: string): string {\n  return `${first} ${last}`;\n}\n\n\u002F\u002F 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\u002F\u002F 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 \u003C numbers.length; i++) {\n    sum += numbers[i];\n  }\n  return sum;\n}\n\n\u002F\u002F 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\u002F\u002F Express middleware must match Express's interface\napp.get('\u002Fusers\u002F:id', async (req, res) => {\n  \u002F\u002F 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\u002F\u002F If team doesn't know fp-ts, this is harder to maintain\nconst processOrder = (order: Order): TE.TaskEither\u003CError, Result> =>\n  pipe(\n    validateOrder(order),\n    TE.fromEither,\n    TE.flatMap(enrichOrder),\n    TE.flatMap(submitOrder)\n  );\n\n\u002F\u002F Familiar to all TypeScript developers\nasync function processOrder(order: Order): Promise\u003CResult> {\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\u002F\u002F This is fine\nconst name = user?.name ?? 'Anonymous';\n\n\u002F\u002F 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\u002Flog anyway and don't need error composition:\n\n```typescript\n\u002F\u002F If this is your error handling anyway...\ntry {\n  await doSomething();\n} catch (e) {\n  logger.error(e);\n  throw e;\n}\n\n\u002F\u002F ...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\u002F\u002F 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\u002F\u002F 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\u002Fawait` | `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.\n","","imported","https:\u002F\u002Fgithub.com\u002Fsickn33\u002Fantigravity-awesome-skills","user_system_seed","SkillOPIC",true,171,2080,"2026-05-16 13:18:49",{"id":8,"name":21,"slug":22,"icon":23,"description":24,"sort":25,"createdAt":26},"其他","other","mdi-page-next-outline","其他类型Skill",5,"2026-05-16 12:53:40",{"id":7,"name":28,"slug":29,"icon":30,"description":31,"moduleId":8,"sort":32,"skillCount":33,"createdAt":26},"职场发展","career","mdi-briefcase-outline","面试准备、简历优化、职业规划",4,575,[35],{"id":36,"skillId":4,"version":37,"fileName":38,"fileSize":39,"filePath":40,"fileHash":41,"manifest":42,"createdAt":19},"b9c48220-ab56-4b85-8d23-30805782e252","1.0.0","fp-refactor.zip",12498,"uploads\u002Fskills\u002F8a1956cd-e4f6-4e9c-b6f8-27147a25ee21\u002Ffp-refactor.zip","e6efe95cb56e9ea6926651c47def9286b2ebebcb640ae8d0df1a0ce2bddc68b3","[{\"path\":\"SKILL.md\",\"isDirectory\":false,\"size\":46127}]",{"code":44,"message":45,"data":46},200,"success",{"items":47,"stats":48,"page":51},[],{"averageRating":49,"totalRatings":49,"ratingCounts":50},0,[49,49,49,49,49],{"limit":52,"offset":49,"hasMore":53,"nextOffset":52,"ratedOnly":16},15,false]