[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6EYK1D4NACM5G79VC55EPDF":3,"skill-same:01M6EYK1D4NACM5G79VC55EPDF":79},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":50,"commitSha":51,"skillDirectory":52,"forksCount":53,"repositoryHtmlUrl":54,"repositoryHomepageUrl":55,"ownerHtmlUrl":56,"ownerBlogUrl":6,"ownerDescription":57,"updatedAt":58,"install":59,"resources":72},"01M6EYK1D4NACM5G79VC55EPDF","fp-ts-pragmatic",null,"A practical, jargon-free guide to fp-ts functional programming - the 80\u002F20 approach that gets results without the academic overhead. Use when writing TypeScript with fp-ts library.","ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",66,"MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fsickn33\u002Ffp-ts-pragmatic\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"pragmatic","fp ts pragmatic","use","with","writing typescript","functional","ts functional","free","practical","fp-ts","writing","guide","results","programming","library","https","functional programming","github","typescript","safe","2026-11-10 10:12:30",false,"---\nname: fp-ts-pragmatic\ndescription: A practical, jargon-free guide to fp-ts functional programming - the 80\u002F20 approach that gets results without the academic overhead. Use when writing TypeScript with fp-ts library.\nrisk: safe\nsource: https:\u002F\u002Fgithub.com\u002Fwhatiskadudoing\u002Ffp-ts-skills\n---\n\n# Pragmatic Functional Programming\n\n**Read this first.** This guide cuts through the academic jargon and shows you what actually matters. No category theory. No abstract nonsense. Just patterns that make your code better.\n\n## When to Use This Skill\n\n- When starting with fp-ts and need practical guidance\n- When writing TypeScript code that handles nullable values, errors, or async operations\n- When you want cleaner, more maintainable functional code without the academic overhead\n- When refactoring imperative code to functional style\n\n## The Golden Rule\n\n> **If functional programming makes your code harder to read, don't use it.**\n\nFP is a tool, not a religion. Use it when it helps. Skip it when it doesn't.\n\n---\n\n## The 80\u002F20 of FP\n\nThese five patterns give you most of the benefits. Master these before exploring anything else.\n\n### 1. Pipe: Chain Operations Clearly\n\nInstead of nesting function calls or creating intermediate variables, chain operations in reading order.\n\n```typescript\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F Before: Hard to read (inside-out)\nconst result = format(validate(parse(input)))\n\n\u002F\u002F Before: Too many variables\nconst parsed = parse(input)\nconst validated = validate(parsed)\nconst result = format(validated)\n\n\u002F\u002F After: Clear, linear flow\nconst result = pipe(\n  input,\n  parse,\n  validate,\n  format\n)\n```\n\n**When to use pipe:**\n- 3+ transformations on the same data\n- You find yourself naming throwaway variables\n- Logic reads better top-to-bottom\n\n**When to skip pipe:**\n- Just 1-2 operations (direct call is fine)\n- The operations don't naturally chain\n\n### 2. Option: Handle Missing Values Without null Checks\n\nStop writing `if (x !== null && x !== undefined)` everywhere.\n\n```typescript\nimport * as O from 'fp-ts\u002FOption'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F Before: Defensive null checking\nfunction getUserCity(user: User | null): string {\n  if (user === null) return 'Unknown'\n  if (user.address === null) return 'Unknown'\n  if (user.address.city === null) return 'Unknown'\n  return user.address.city\n}\n\n\u002F\u002F After: Chain through potential missing values\nconst getUserCity = (user: User | null): string =>\n  pipe(\n    O.fromNullable(user),\n    O.flatMap(u => O.fromNullable(u.address)),\n    O.flatMap(a => O.fromNullable(a.city)),\n    O.getOrElse(() => 'Unknown')\n  )\n```\n\n**Plain language translation:**\n- `O.fromNullable(x)` = \"wrap this value, treating null\u002Fundefined as 'nothing'\"\n- `O.flatMap(fn)` = \"if we have something, apply this function\"\n- `O.getOrElse(() => default)` = \"unwrap, or use this default if nothing\"\n\n### 3. Either: Make Errors Explicit\n\nStop throwing exceptions for expected failures. Return errors as values.\n\n```typescript\nimport * as E from 'fp-ts\u002FEither'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F Before: Hidden failure mode\nfunction parseAge(input: string): number {\n  const age = parseInt(input, 10)\n  if (isNaN(age)) throw new Error('Invalid age')\n  if (age \u003C 0) throw new Error('Age cannot be negative')\n  return age\n}\n\n\u002F\u002F After: Errors are visible in the type\nfunction parseAge(input: string): E.Either\u003Cstring, number> {\n  const age = parseInt(input, 10)\n  if (isNaN(age)) return E.left('Invalid age')\n  if (age \u003C 0) return E.left('Age cannot be negative')\n  return E.right(age)\n}\n\n\u002F\u002F Using it\nconst result = parseAge(userInput)\nif (E.isRight(result)) {\n  console.log(`Age is ${result.right}`)\n} else {\n  console.log(`Error: ${result.left}`)\n}\n```\n\n**Plain language translation:**\n- `E.right(value)` = \"success with this value\"\n- `E.left(error)` = \"failure with this error\"\n- `E.isRight(x)` = \"did it succeed?\"\n\n### 4. Map: Transform Without Unpacking\n\nTransform values inside containers without extracting them first.\n\n```typescript\nimport * as O from 'fp-ts\u002FOption'\nimport * as E from 'fp-ts\u002FEither'\nimport * as A from 'fp-ts\u002FArray'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F Transform inside Option\nconst maybeUser: O.Option\u003CUser> = O.some({ name: 'Alice', age: 30 })\nconst maybeName: O.Option\u003Cstring> = pipe(\n  maybeUser,\n  O.map(user => user.name)\n)\n\n\u002F\u002F Transform inside Either\nconst result: E.Either\u003CError, number> = E.right(5)\nconst doubled: E.Either\u003CError, number> = pipe(\n  result,\n  E.map(n => n * 2)\n)\n\n\u002F\u002F Transform arrays (same concept!)\nconst numbers = [1, 2, 3]\nconst doubled = pipe(\n  numbers,\n  A.map(n => n * 2)\n)\n```\n\n### 5. FlatMap: Chain Operations That Might Fail\n\nWhen each step might fail, chain them together.\n\n```typescript\nimport * as E from 'fp-ts\u002FEither'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\nconst parseJSON = (s: string): E.Either\u003Cstring, unknown> =>\n  E.tryCatch(() => JSON.parse(s), () => 'Invalid JSON')\n\nconst extractEmail = (data: unknown): E.Either\u003Cstring, string> => {\n  if (typeof data === 'object' && data !== null && 'email' in data) {\n    return E.right((data as { email: string }).email)\n  }\n  return E.left('No email field')\n}\n\nconst validateEmail = (email: string): E.Either\u003Cstring, string> =>\n  email.includes('@') ? E.right(email) : E.left('Invalid email format')\n\n\u002F\u002F Chain all steps - if any fails, the whole thing fails\nconst getValidEmail = (input: string): E.Either\u003Cstring, string> =>\n  pipe(\n    parseJSON(input),\n    E.flatMap(extractEmail),\n    E.flatMap(validateEmail)\n  )\n\n\u002F\u002F Success path: Right('user@example.com')\n\u002F\u002F Any failure: Left('specific error message')\n```\n\n**Plain language:** `flatMap` means \"if this succeeded, try the next thing\"\n\n---\n\n## When NOT to Use FP\n\nFunctional programming is not always the answer. Here's when to keep it simple.\n\n### Simple Null Checks\n\n```typescript\n\u002F\u002F Just use optional chaining - it's built into the language\nconst city = user?.address?.city ?? 'Unknown'\n\n\u002F\u002F DON'T overcomplicate it\nconst city = pipe(\n  O.fromNullable(user),\n  O.flatMap(u => O.fromNullable(u.address)),\n  O.flatMap(a => O.fromNullable(a.city)),\n  O.getOrElse(() => 'Unknown')\n)\n```\n\n### Simple Loops\n\n```typescript\n\u002F\u002F A for loop is fine when you need early exit or complex logic\nfunction findFirst(items: Item[], predicate: (i: Item) => boolean): Item | null {\n  for (const item of items) {\n    if (predicate(item)) return item\n  }\n  return null\n}\n\n\u002F\u002F DON'T force FP when it doesn't help\nconst result = pipe(\n  items,\n  A.findFirst(predicate),\n  O.toNullable\n)\n```\n\n### Performance-Critical Code\n\n```typescript\n\u002F\u002F For hot paths, imperative is faster (no intermediate arrays)\nfunction sumLarge(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 fp-ts creates intermediate structures\nconst sum = pipe(numbers, A.reduce(0, (acc, n) => acc + n))\n```\n\n### When Your Team Doesn't Know FP\n\nIf you're the only one who can read the code, it's not good code.\n\n```typescript\n\u002F\u002F If your team knows this pattern\nasync function getUser(id: string): Promise\u003CUser | null> {\n  try {\n    const response = await fetch(`\u002Fapi\u002Fusers\u002F${id}`)\n    if (!response.ok) return null\n    return await response.json()\n  } catch {\n    return null\n  }\n}\n\n\u002F\u002F Don't force this on them\nconst getUser = (id: string): TE.TaskEither\u003CError, User> =>\n  pipe(\n    TE.tryCatch(() => fetch(`\u002Fapi\u002Fusers\u002F${id}`), E.toError),\n    TE.flatMap(r => r.ok ? TE.right(r) : TE.left(new Error('Not found'))),\n    TE.flatMap(r => TE.tryCatch(() => r.json(), E.toError))\n  )\n```\n\n---\n\n## Quick Wins: Easy Changes That Improve Code Today\n\n### 1. Replace Nested Ternaries with pipe + fold\n\n```typescript\n\u002F\u002F Before: Nested ternary nightmare\nconst message = user === null\n  ? 'No user'\n  : user.isAdmin\n    ? `Admin: ${user.name}`\n    : `User: ${user.name}`\n\n\u002F\u002F After: Clear case handling\nconst message = pipe(\n  O.fromNullable(user),\n  O.fold(\n    () => 'No user',\n    (u) => u.isAdmin ? `Admin: ${u.name}` : `User: ${u.name}`\n  )\n)\n```\n\n### 2. Replace try-catch with tryCatch\n\n```typescript\n\u002F\u002F Before: try-catch everywhere\nlet config\ntry {\n  config = JSON.parse(rawConfig)\n} catch {\n  config = defaultConfig\n}\n\n\u002F\u002F After: One-liner\nconst config = pipe(\n  E.tryCatch(() => JSON.parse(rawConfig), () => 'parse error'),\n  E.getOrElse(() => defaultConfig)\n)\n```\n\n### 3. Replace undefined Returns with Option\n\n```typescript\n\u002F\u002F Before: Caller might forget to check\nfunction findUser(id: string): User | undefined {\n  return users.find(u => u.id === id)\n}\n\n\u002F\u002F After: Type forces caller to handle missing case\nfunction findUser(id: string): O.Option\u003CUser> {\n  return O.fromNullable(users.find(u => u.id === id))\n}\n```\n\n### 4. Replace Error Strings with Typed Errors\n\n```typescript\n\u002F\u002F Before: Just strings\nfunction validate(data: unknown): E.Either\u003Cstring, User> {\n  \u002F\u002F ...\n  return E.left('validation failed')\n}\n\n\u002F\u002F After: Structured errors\ntype ValidationError = {\n  field: string\n  message: string\n}\n\nfunction validate(data: unknown): E.Either\u003CValidationError, User> {\n  \u002F\u002F ...\n  return E.left({ field: 'email', message: 'Invalid format' })\n}\n```\n\n### 5. Use const Assertions for Error Types\n\n```typescript\n\u002F\u002F Create specific error types without classes\nconst NotFound = (id: string) => ({ _tag: 'NotFound' as const, id })\nconst Unauthorized = { _tag: 'Unauthorized' as const }\nconst ValidationFailed = (errors: string[]) =>\n  ({ _tag: 'ValidationFailed' as const, errors })\n\ntype AppError =\n  | ReturnType\u003Ctypeof NotFound>\n  | typeof Unauthorized\n  | ReturnType\u003Ctypeof ValidationFailed>\n\n\u002F\u002F Now you can pattern match\nconst handleError = (error: AppError): string => {\n  switch (error._tag) {\n    case 'NotFound': return `Item ${error.id} not found`\n    case 'Unauthorized': return 'Please log in'\n    case 'ValidationFailed': return error.errors.join(', ')\n  }\n}\n```\n\n---\n\n## Common Refactors: Before and After\n\n### Callback Hell to Pipe\n\n```typescript\n\u002F\u002F Before\nfetchUser(id, (user) => {\n  if (!user) return handleNoUser()\n  fetchPosts(user.id, (posts) => {\n    if (!posts) return handleNoPosts()\n    fetchComments(posts[0].id, (comments) => {\n      render(user, posts, comments)\n    })\n  })\n})\n\n\u002F\u002F After (with TaskEither for async)\nimport * as TE from 'fp-ts\u002FTaskEither'\n\nconst loadData = (id: string) =>\n  pipe(\n    fetchUser(id),\n    TE.flatMap(user => pipe(\n      fetchPosts(user.id),\n      TE.map(posts => ({ user, posts }))\n    )),\n    TE.flatMap(({ user, posts }) => pipe(\n      fetchComments(posts[0].id),\n      TE.map(comments => ({ user, posts, comments }))\n    ))\n  )\n\n\u002F\u002F Execute\nconst result = await loadData('123')()\npipe(\n  result,\n  E.fold(handleError, ({ user, posts, comments }) => render(user, posts, comments))\n)\n```\n\n### Multiple null Checks to Option Chain\n\n```typescript\n\u002F\u002F Before\nfunction getManagerEmail(employee: Employee): string | null {\n  if (!employee.department) return null\n  if (!employee.department.manager) return null\n  if (!employee.department.manager.email) return null\n  return employee.department.manager.email\n}\n\n\u002F\u002F After\nconst getManagerEmail = (employee: Employee): O.Option\u003Cstring> =>\n  pipe(\n    O.fromNullable(employee.department),\n    O.flatMap(d => O.fromNullable(d.manager)),\n    O.flatMap(m => O.fromNullable(m.email))\n  )\n\n\u002F\u002F Use it\npipe(\n  getManagerEmail(employee),\n  O.fold(\n    () => sendToDefault(),\n    (email) => sendTo(email)\n  )\n)\n```\n\n### Validation with Multiple Checks\n\n```typescript\n\u002F\u002F Before: Throws on first error\nfunction validateUser(data: unknown): User {\n  if (!data || typeof data !== 'object') throw new Error('Must be object')\n  const obj = data as Record\u003Cstring, unknown>\n  if (typeof obj.email !== 'string') throw new Error('Email required')\n  if (!obj.email.includes('@')) throw new Error('Invalid email')\n  if (typeof obj.age !== 'number') throw new Error('Age required')\n  if (obj.age \u003C 0) throw new Error('Age must be positive')\n  return obj as User\n}\n\n\u002F\u002F After: Returns first error, type-safe\nconst validateUser = (data: unknown): E.Either\u003Cstring, User> =>\n  pipe(\n    E.Do,\n    E.bind('obj', () =>\n      typeof data === 'object' && data !== null\n        ? E.right(data as Record\u003Cstring, unknown>)\n        : E.left('Must be object')\n    ),\n    E.bind('email', ({ obj }) =>\n      typeof obj.email === 'string' && obj.email.includes('@')\n        ? E.right(obj.email)\n        : E.left('Valid email required')\n    ),\n    E.bind('age', ({ obj }) =>\n      typeof obj.age === 'number' && obj.age >= 0\n        ? E.right(obj.age)\n        : E.left('Valid age required')\n    ),\n    E.map(({ email, age }) => ({ email, age }))\n  )\n```\n\n### Promise Chain to TaskEither\n\n```typescript\n\u002F\u002F Before\nasync function processOrder(orderId: string): Promise\u003CReceipt> {\n  const order = await fetchOrder(orderId)\n  if (!order) throw new Error('Order not found')\n\n  const validated = await validateOrder(order)\n  if (!validated.success) throw new Error(validated.error)\n\n  const payment = await processPayment(validated.order)\n  if (!payment.success) throw new Error('Payment failed')\n\n  return generateReceipt(payment)\n}\n\n\u002F\u002F After\nconst processOrder = (orderId: string): TE.TaskEither\u003Cstring, Receipt> =>\n  pipe(\n    fetchOrderTE(orderId),\n    TE.flatMap(order =>\n      order ? TE.right(order) : TE.left('Order not found')\n    ),\n    TE.flatMap(validateOrderTE),\n    TE.flatMap(processPaymentTE),\n    TE.map(generateReceipt)\n  )\n```\n\n---\n\n## The Readability Rule\n\nBefore using any FP pattern, ask: **\"Would a junior developer understand this?\"**\n\n### Too Clever (Avoid)\n\n```typescript\nconst result = pipe(\n  data,\n  A.filter(flow(prop('status'), equals('active'))),\n  A.map(flow(prop('value'), multiply(2))),\n  A.reduce(monoid.concat, monoid.empty),\n  O.fromPredicate(gt(threshold))\n)\n```\n\n### Just Right (Prefer)\n\n```typescript\nconst activeItems = data.filter(item => item.status === 'active')\nconst doubledValues = activeItems.map(item => item.value * 2)\nconst total = doubledValues.reduce((sum, val) => sum + val, 0)\nconst result = total > threshold ? O.some(total) : O.none\n```\n\n### The Middle Ground (Often Best)\n\n```typescript\nconst result = pipe(\n  data,\n  A.filter(item => item.status === 'active'),\n  A.map(item => item.value * 2),\n  A.reduce(0, (sum, val) => sum + val),\n  total => total > threshold ? O.some(total) : O.none\n)\n```\n\n---\n\n## Cheat Sheet\n\n| What you want | Plain language | fp-ts |\n|--------------|----------------|-------|\n| Handle null\u002Fundefined | \"Wrap this nullable\" | `O.fromNullable(x)` |\n| Default for missing | \"Use this if nothing\" | `O.getOrElse(() => default)` |\n| Transform if present | \"If something, change it\" | `O.map(fn)` |\n| Chain nullable operations | \"If something, try this\" | `O.flatMap(fn)` |\n| Return success | \"Worked, here's the value\" | `E.right(value)` |\n| Return failure | \"Failed, here's why\" | `E.left(error)` |\n| Wrap throwing function | \"Try this, catch errors\" | `E.tryCatch(fn, onError)` |\n| Handle both cases | \"Do this for error, that for success\" | `E.fold(onLeft, onRight)` |\n| Chain operations | \"Then do this, then that\" | `pipe(x, fn1, fn2, fn3)` |\n\n---\n\n## When to Level Up\n\nOnce comfortable with these patterns, explore:\n\n1. **TaskEither** - Async operations that can fail (replaces Promise + try\u002Fcatch)\n2. **Validation** - Collect ALL errors instead of stopping at first\n3. **Reader** - Dependency injection without classes\n4. **Do notation** - Cleaner syntax for multiple bindings\n\nBut don't rush. The basics here will handle 80% of real-world scenarios. Get comfortable with these before adding more tools to your belt.\n\n---\n\n## Summary\n\n1. **Use pipe** for 3+ operations\n2. **Use Option** for nullable chains\n3. **Use Either** for operations that can fail\n4. **Use map** to transform wrapped values\n5. **Use flatMap** to chain operations that might fail\n6. **Skip FP** when it hurts readability\n7. **Keep it simple** - if your team can't read it, it's not good code\n","# Pragmatic Functional Programming\n\n**Read this first.** This guide cuts through the academic jargon and shows you what actually matters. No category theory. No abstract nonsense. Just patterns that make your code better.\n\n## When to Use This Skill\n\n- When starting with fp-ts and need practical guidance\n- When writing TypeScript code that handles nullable values, errors, or async operations\n- When you want cleaner, more maintainable functional code without the academic overhead\n- When refactoring imperative code to functional style\n\n## The Golden Rule\n\n> **If functional programming makes your code harder to read, don't use it.**\n\nFP is a tool, not a religion. Use it when it helps. Skip it when it doesn't.\n\n---\n\n## The 80\u002F20 of FP\n\nThese five patterns give you most of the benefits. Master these before exploring anything else.\n\n### 1. Pipe: Chain Operations Clearly\n\nInstead of nesting function calls or creating intermediate variables, chain operations in reading order.\n\n```typescript\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F Before: Hard to read (inside-out)\nconst result = format(validate(parse(input)))\n\n\u002F\u002F Before: Too many variables\nconst parsed = parse(input)\nconst validated = validate(parsed)\nconst result = format(validated)\n\n\u002F\u002F After: Clear, linear flow\nconst result = pipe(\n  input,\n  parse,\n  validate,\n  format\n)\n```\n\n**When to use pipe:**\n- 3+ transformations on the same data\n- You find yourself naming throwaway variables\n- Logic reads better top-to-bottom\n\n**When to skip pipe:**\n- Just 1-2 operations (direct call is fine)\n- The operations don't naturally chain\n\n### 2. Option: Handle Missing Values Without null Checks\n\nStop writing `if (x !== null && x !== undefined)` everywhere.\n\n```typescript\nimport * as O from 'fp-ts\u002FOption'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F Before: Defensive null checking\nfunction getUserCity(user: User | null): string {\n  if (user === null) return 'Unknown'\n  if (user.address === null) return 'Unknown'\n  if (user.address.city === null) return 'Unknown'\n  return user.address.city\n}\n\n\u002F\u002F After: Chain through potential missing values\nconst getUserCity = (user: User | null): string =>\n  pipe(\n    O.fromNullable(user),\n    O.flatMap(u => O.fromNullable(u.address)),\n    O.flatMap(a => O.fromNullable(a.city)),\n    O.getOrElse(() => 'Unknown')\n  )\n```\n\n**Plain language translation:**\n- `O.fromNullable(x)` = \"wrap this value, treating null\u002Fundefined as 'nothing'\"\n- `O.flatMap(fn)` = \"if we have something, apply this function\"\n- `O.getOrElse(() => default)` = \"unwrap, or use this default if nothing\"\n\n### 3. Either: Make Errors Explicit\n\nStop throwing exceptions for expected failures. Return errors as values.\n\n```typescript\nimport * as E from 'fp-ts\u002FEither'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F Before: Hidden failure mode\nfunction parseAge(input: string): number {\n  const age = parseInt(input, 10)\n  if (isNaN(age)) throw new Error('Invalid age')\n  if (age \u003C 0) throw new Error('Age cannot be negative')\n  return age\n}\n\n\u002F\u002F After: Errors are visible in the type\nfunction parseAge(input: string): E.Either\u003Cstring, number> {\n  const age = parseInt(input, 10)\n  if (isNaN(age)) return E.left('Invalid age')\n  if (age \u003C 0) return E.left('Age cannot be negative')\n  return E.right(age)\n}\n\n\u002F\u002F Using it\nconst result = parseAge(userInput)\nif (E.isRight(result)) {\n  console.log(`Age is ${result.right}`)\n} else {\n  console.log(`Error: ${result.left}`)\n}\n```\n\n**Plain language translation:**\n- `E.right(value)` = \"success with this value\"\n- `E.left(error)` = \"failure with this error\"\n- `E.isRight(x)` = \"did it succeed?\"\n\n### 4. Map: Transform Without Unpacking\n\nTransform values inside containers without extracting them first.\n\n```typescript\nimport * as O from 'fp-ts\u002FOption'\nimport * as E from 'fp-ts\u002FEither'\nimport * as A from 'fp-ts\u002FArray'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\n\u002F\u002F Transform inside Option\nconst maybeUser: O.Option\u003CUser> = O.some({ name: 'Alice', age: 30 })\nconst maybeName: O.Option\u003Cstring> = pipe(\n  maybeUser,\n  O.map(user => user.name)\n)\n\n\u002F\u002F Transform inside Either\nconst result: E.Either\u003CError, number> = E.right(5)\nconst doubled: E.Either\u003CError, number> = pipe(\n  result,\n  E.map(n => n * 2)\n)\n\n\u002F\u002F Transform arrays (same concept!)\nconst numbers = [1, 2, 3]\nconst doubled = pipe(\n  numbers,\n  A.map(n => n * 2)\n)\n```\n\n### 5. FlatMap: Chain Operations That Might Fail\n\nWhen each step might fail, chain them together.\n\n```typescript\nimport * as E from 'fp-ts\u002FEither'\nimport { pipe } from 'fp-ts\u002Ffunction'\n\nconst parseJSON = (s: string): E.Either\u003Cstring, unknown> =>\n  E.tryCatch(() => JSON.parse(s), () => 'Invalid JSON')\n\nconst extractEmail = (data: unknown): E.Either\u003Cstring, string> => {\n  if (typeof data === 'object' && data !== null && 'email' in data) {\n    return E.right((data as { email: string }).email)\n  }\n  return E.left('No email field')\n}\n\nconst validateEmail = (email: string): E.Either\u003Cstring, string> =>\n  email.includes('@') ? E.right(email) : E.left('Invalid email format')\n\n\u002F\u002F Chain all steps - if any fails, the whole thing fails\nconst getValidEmail = (input: string): E.Either\u003Cstring, string> =>\n  pipe(\n    parseJSON(input),\n    E.flatMap(extractEmail),\n    E.flatMap(validateEmail)\n  )\n\n\u002F\u002F Success path: Right('user@example.com')\n\u002F\u002F Any failure: Left('specific error message')\n```\n\n**Plain language:** `flatMap` means \"if this succeeded, try the next thing\"\n\n---\n\n## When NOT to Use FP\n\nFunctional programming is not always the answer. Here's when to keep it simple.\n\n### Simple Null Checks\n\n```typescript\n\u002F\u002F Just use optional chaining - it's built into the language\nconst city = user?.address?.city ?? 'Unknown'\n\n\u002F\u002F DON'T overcomplicate it\nconst city = pipe(\n  O.fromNullable(user),\n  O.flatMap(u => O.fromNullable(u.address)),\n  O.flatMap(a => O.fromNullable(a.city)),\n  O.getOrElse(() => 'Unknown')\n)\n```\n\n### Simple Loops\n\n```typescript\n\u002F\u002F A for loop is fine when you need early exit or complex logic\nfunction findFirst(items: Item[], predicate: (i: Item) => boolean): Item | null {\n  for (const item of items) {\n    if (predicate(item)) return item\n  }\n  return null\n}\n\n\u002F\u002F DON'T force FP when it doesn't help\nconst result = pipe(\n  items,\n  A.findFirst(predicate),\n  O.toNullable\n)\n```\n\n### Performance-Critical Code\n\n```typescript\n\u002F\u002F For hot paths, imperative is faster (no intermediate arrays)\nfunction sumLarge(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 fp-ts creates intermediate structures\nconst sum = pipe(numbers, A.reduce(0, (acc, n) => acc + n))\n```\n\n### When Your Team Doesn't Know FP\n\nIf you're the only one who can read the code, it's not good code.\n\n```typescript\n\u002F\u002F If your team knows this pattern\nasync function getUser(id: string): Promise\u003CUser | null> {\n  try {\n    const response = await fetch(`\u002Fapi\u002Fusers\u002F${id}`)\n    if (!response.ok) return null\n    return await response.json()\n  } catch {\n    return null\n  }\n}\n\n\u002F\u002F Don't force this on them\nconst getUser = (id: string): TE.TaskEither\u003CError, User> =>\n  pipe(\n    TE.tryCatch(() => fetch(`\u002Fapi\u002Fusers\u002F${id}`), E.toError),\n    TE.flatMap(r => r.ok ? TE.right(r) : TE.left(new Error('Not found'))),\n    TE.flatMap(r => TE.tryCatch(() => r.json(), E.toError))\n  )\n```\n\n---\n\n## Quick Wins: Easy Changes That Improve Code Today\n\n### 1. Replace Nested Ternaries with pipe + fold\n\n```typescript\n\u002F\u002F Before: Nested ternary nightmare\nconst message = user === null\n  ? 'No user'\n  : user.isAdmin\n    ? `Admin: ${user.name}`\n    : `User: ${user.name}`\n\n\u002F\u002F After: Clear case handling\nconst message = pipe(\n  O.fromNullable(user),\n  O.fold(\n    () => 'No user',\n    (u) => u.isAdmin ? `Admin: ${u.name}` : `User: ${u.name}`\n  )\n)\n```\n\n### 2. Replace try-catch with tryCatch\n\n```typescript\n\u002F\u002F Before: try-catch everywhere\nlet config\ntry {\n  config = JSON.parse(rawConfig)\n} catch {\n  config = defaultConfig\n}\n\n\u002F\u002F After: One-liner\nconst config = pipe(\n  E.tryCatch(() => JSON.parse(rawConfig), () => 'parse error'),\n  E.getOrElse(() => defaultConfig)\n)\n```\n\n### 3. Replace undefined Returns with Option\n\n```typescript\n\u002F\u002F Before: Caller might forget to check\nfunction findUser(id: string): User | undefined {\n  return users.find(u => u.id === id)\n}\n\n\u002F\u002F After: Type forces caller to handle missing case\nfunction findUser(id: string): O.Option\u003CUser> {\n  return O.fromNullable(users.find(u => u.id === id))\n}\n```\n\n### 4. Replace Error Strings with Typed Errors\n\n```typescript\n\u002F\u002F Before: Just strings\nfunction validate(data: unknown): E.Either\u003Cstring, User> {\n  \u002F\u002F ...\n  return E.left('validation failed')\n}\n\n\u002F\u002F After: Structured errors\ntype ValidationError = {\n  field: string\n  message: string\n}\n\nfunction validate(data: unknown): E.Either\u003CValidationError, User> {\n  \u002F\u002F ...\n  return E.left({ field: 'email', message: 'Invalid format' })\n}\n```\n\n### 5. Use const Assertions for Error Types\n\n```typescript\n\u002F\u002F Create specific error types without classes\nconst NotFound = (id: string) => ({ _tag: 'NotFound' as const, id })\nconst Unauthorized = { _tag: 'Unauthorized' as const }\nconst ValidationFailed = (errors: string[]) =>\n  ({ _tag: 'ValidationFailed' as const, errors })\n\ntype AppError =\n  | ReturnType\u003Ctypeof NotFound>\n  | typeof Unauthorized\n  | ReturnType\u003Ctypeof ValidationFailed>\n\n\u002F\u002F Now you can pattern match\nconst handleError = (error: AppError): string => {\n  switch (error._tag) {\n    case 'NotFound': return `Item ${error.id} not found`\n    case 'Unauthorized': return 'Please log in'\n    case 'ValidationFailed': return error.errors.join(', ')\n  }\n}\n```\n\n---\n\n## Common Refactors: Before and After\n\n### Callback Hell to Pipe\n\n```typescript\n\u002F\u002F Before\nfetchUser(id, (user) => {\n  if (!user) return handleNoUser()\n  fetchPosts(user.id, (posts) => {\n    if (!posts) return handleNoPosts()\n    fetchComments(posts[0].id, (comments) => {\n      render(user, posts, comments)\n    })\n  })\n})\n\n\u002F\u002F After (with TaskEither for async)\nimport * as TE from 'fp-ts\u002FTaskEither'\n\nconst loadData = (id: string) =>\n  pipe(\n    fetchUser(id),\n    TE.flatMap(user => pipe(\n      fetchPosts(user.id),\n      TE.map(posts => ({ user, posts }))\n    )),\n    TE.flatMap(({ user, posts }) => pipe(\n      fetchComments(posts[0].id),\n      TE.map(comments => ({ user, posts, comments }))\n    ))\n  )\n\n\u002F\u002F Execute\nconst result = await loadData('123')()\npipe(\n  result,\n  E.fold(handleError, ({ user, posts, comments }) => render(user, posts, comments))\n)\n```\n\n### Multiple null Checks to Option Chain\n\n```typescript\n\u002F\u002F Before\nfunction getManagerEmail(employee: Employee): string | null {\n  if (!employee.department) return null\n  if (!employee.department.manager) return null\n  if (!employee.department.manager.email) return null\n  return employee.department.manager.email\n}\n\n\u002F\u002F After\nconst getManagerEmail = (employee: Employee): O.Option\u003Cstring> =>\n  pipe(\n    O.fromNullable(employee.department),\n    O.flatMap(d => O.fromNullable(d.manager)),\n    O.flatMap(m => O.fromNullable(m.email))\n  )\n\n\u002F\u002F Use it\npipe(\n  getManagerEmail(employee),\n  O.fold(\n    () => sendToDefault(),\n    (email) => sendTo(email)\n  )\n)\n```\n\n### Validation with Multiple Checks\n\n```typescript\n\u002F\u002F Before: Throws on first error\nfunction validateUser(data: unknown): User {\n  if (!data || typeof data !== 'object') throw new Error('Must be object')\n  const obj = data as Record\u003Cstring, unknown>\n  if (typeof obj.email !== 'string') throw new Error('Email required')\n  if (!obj.email.includes('@')) throw new Error('Invalid email')\n  if (typeof obj.age !== 'number') throw new Error('Age required')\n  if (obj.age \u003C 0) throw new Error('Age must be positive')\n  return obj as User\n}\n\n\u002F\u002F After: Returns first error, type-safe\nconst validateUser = (data: unknown): E.Either\u003Cstring, User> =>\n  pipe(\n    E.Do,\n    E.bind('obj', () =>\n      typeof data === 'object' && data !== null\n        ? E.right(data as Record\u003Cstring, unknown>)\n        : E.left('Must be object')\n    ),\n    E.bind('email', ({ obj }) =>\n      typeof obj.email === 'string' && obj.email.includes('@')\n        ? E.right(obj.email)\n        : E.left('Valid email required')\n    ),\n    E.bind('age', ({ obj }) =>\n      typeof obj.age === 'number' && obj.age >= 0\n        ? E.right(obj.age)\n        : E.left('Valid age required')\n    ),\n    E.map(({ email, age }) => ({ email, age }))\n  )\n```\n\n### Promise Chain to TaskEither\n\n```typescript\n\u002F\u002F Before\nasync function processOrder(orderId: string): Promise\u003CReceipt> {\n  const order = await fetchOrder(orderId)\n  if (!order) throw new Error('Order not found')\n\n  const validated = await validateOrder(order)\n  if (!validated.success) throw new Error(validated.error)\n\n  const payment = await processPayment(validated.order)\n  if (!payment.success) throw new Error('Payment failed')\n\n  return generateReceipt(payment)\n}\n\n\u002F\u002F After\nconst processOrder = (orderId: string): TE.TaskEither\u003Cstring, Receipt> =>\n  pipe(\n    fetchOrderTE(orderId),\n    TE.flatMap(order =>\n      order ? TE.right(order) : TE.left('Order not found')\n    ),\n    TE.flatMap(validateOrderTE),\n    TE.flatMap(processPaymentTE),\n    TE.map(generateReceipt)\n  )\n```\n\n---\n\n## The Readability Rule\n\nBefore using any FP pattern, ask: **\"Would a junior developer understand this?\"**\n\n### Too Clever (Avoid)\n\n```typescript\nconst result = pipe(\n  data,\n  A.filter(flow(prop('status'), equals('active'))),\n  A.map(flow(prop('value'), multiply(2))),\n  A.reduce(monoid.concat, monoid.empty),\n  O.fromPredicate(gt(threshold))\n)\n```\n\n### Just Right (Prefer)\n\n```typescript\nconst activeItems = data.filter(item => item.status === 'active')\nconst doubledValues = activeItems.map(item => item.value * 2)\nconst total = doubledValues.reduce((sum, val) => sum + val, 0)\nconst result = total > threshold ? O.some(total) : O.none\n```\n\n### The Middle Ground (Often Best)\n\n```typescript\nconst result = pipe(\n  data,\n  A.filter(item => item.status === 'active'),\n  A.map(item => item.value * 2),\n  A.reduce(0, (sum, val) => sum + val),\n  total => total > threshold ? O.some(total) : O.none\n)\n```\n\n---\n\n## Cheat Sheet\n\n| What you want | Plain language | fp-ts |\n|--------------|----------------|-------|\n| Handle null\u002Fundefined | \"Wrap this nullable\" | `O.fromNullable(x)` |\n| Default for missing | \"Use this if nothing\" | `O.getOrElse(() => default)` |\n| Transform if present | \"If something, change it\" | `O.map(fn)` |\n| Chain nullable operations | \"If something, try this\" | `O.flatMap(fn)` |\n| Return success | \"Worked, here's the value\" | `E.right(value)` |\n| Return failure | \"Failed, here's why\" | `E.left(error)` |\n| Wrap throwing function | \"Try this, catch errors\" | `E.tryCatch(fn, onError)` |\n| Handle both cases | \"Do this for error, that for success\" | `E.fold(onLeft, onRight)` |\n| Chain operations | \"Then do this, then that\" | `pipe(x, fn1, fn2, fn3)` |\n\n---\n\n## When to Level Up\n\nOnce comfortable with these patterns, explore:\n\n1. **TaskEither** - Async operations that can fail (replaces Promise + try\u002Fcatch)\n2. **Validation** - Collect ALL errors instead of stopping at first\n3. **Reader** - Dependency injection without classes\n4. **Do notation** - Cleaner syntax for multiple bindings\n\nBut don't rush. The basics here will handle 80% of real-world scenarios. Get comfortable with these before adding more tools to your belt.\n\n---\n\n## Summary\n\n1. **Use pipe** for 3+ operations\n2. **Use Option** for nullable chains\n3. **Use Either** for operations that can fail\n4. **Use map** to transform wrapped values\n5. **Use flatMap** to chain operations that might fail\n6. **Skip FP** when it hurts readability\n7. **Keep it simple** - if your team can't read it, it's not good code\n",{"name":5,"risk":43,"source":49,"description":7},"https:\u002F\u002Fgithub.com\u002Fwhatiskadudoing\u002Ffp-ts-skills","main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002Faiskillstore\u002Fmarketplace\u002Fsickn33\u002Ffp-ts-pragmatic",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":8,"skillName":5,"commands":60,"prompt":70,"zipAvailable":71},[61,64,67],{"tool":62,"command":63},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill fp-ts-pragmatic",{"tool":65,"command":66},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill fp-ts-pragmatic",{"tool":68,"command":69},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill fp-ts-pragmatic","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fsickn33\u002Ffp-ts-pragmatic\u002FSKILL.md",true,[73],{"path":74,"resourceType":75,"resourceTypeDesc":76,"sizeBytes":77,"sourceHtmlUrl":78},"skill-report.json","REFERENCE","参考文档",8646,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Faiskillstore\u002Fmarketplace\u002Fsickn33\u002Ffp-ts-pragmatic\u002Fskill-report.json",[80,94,104,113,119,126],{"skillNo":81,"name":82,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":83,"categories":84,"keywords":90,"publishedAt":44},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[85],{"categoryNo":86,"categoryName":87,"taxonomyType":88,"taxonomyTypeDesc":89},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[91,92,93],"model","model hierarchy","model hierarchy skill",{"skillNo":95,"name":96,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":97,"categories":98,"keywords":100,"publishedAt":44},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[99],{"categoryNo":86,"categoryName":87,"taxonomyType":88,"taxonomyTypeDesc":89},[101,102,103],"MCP","zoom","zoom mcp",{"skillNo":105,"name":106,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":107,"categories":108,"keywords":109,"publishedAt":44},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[110,111,112,102],"sdk","zoom apps sdk","apps",{"skillNo":114,"name":115,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":116,"categories":117,"keywords":118,"publishedAt":44},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[115],{"skillNo":120,"name":121,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":122,"categories":123,"keywords":125,"publishedAt":44},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[124],{"categoryNo":86,"categoryName":87,"taxonomyType":88,"taxonomyTypeDesc":89},[121],{"skillNo":127,"name":128,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":129,"categories":130,"keywords":131,"publishedAt":44},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[128]]