Refactor TypeScript to fp-ts
Refactoring Imperative Code to fp-ts covers converting try/catch, nulls, callbacks, class DI, and loops - plus when NOT to refactor.
Why it matters
Migrate your imperative TypeScript codebase to robust functional programming patterns using fp-ts. This skill provides comprehensive strategies and patterns for handling common refactoring tasks, improving code quality and maintainability.
Outcomes
What it gets done
Convert try-catch blocks to Either/TaskEither for explicit error handling.
Refactor null checks into Option types for safer data handling.
Transform callbacks and Promise chains into functional Task/TaskEither equivalents.
Guide the conversion of class-based Dependency Injection to fp-ts Reader patterns.
Install
Add it to your toolbox
Run in your project directory:
curl -fsSL https://spark.entire.vc/get/ag-fp-refactor | bash Overview
Refactoring Imperative Code to fp-ts
Provides migration patterns and honest tradeoffs for converting imperative TypeScript (try/catch, null checks, callbacks, class DI, loops, Promise chains) to fp-ts, plus common pitfalls, gradual adoption strategies, and explicit guidance on when not to refactor. Use when migrating an existing imperative TypeScript codebase to fp-ts - not as a source of isolated fp-ts examples, and heed the explicit guidance on when refactoring adds complexity without benefit.
What it does
Refactoring Imperative Code to fp-ts provides comprehensive migration patterns and honest tradeoffs for moving existing imperative TypeScript toward fp-ts, rather than isolated functional-programming examples. It covers six conversion domains plus pitfalls, adoption strategy, and explicit "don't bother" guidance.
Converting try/catch to Either/TaskEither walks a synchronous JSON-parsing-and-validation example from nested try/catch to an Either-returning pipeline (E.left/E.right, E.flatMap, E.match), with a six-step refactoring guide (identify the error type, change the return type, replace throws with E.left, replace returns with E.right, remove the try/catch, update callers to pipe+E.flatMap), then repeats the pattern for async code with TaskEither, chaining a user-plus-posts fetch via TE.Do/TE.bind instead of nested try/catch, plus reusable tryCatchSync/tryCatchAsync wrappers.
Converting null checks to Option covers a deeply nested optional config example (database host/port/credentials) rewritten with O.fromNullable and O.flatMap chains, an array-find example (A.findFirst returning Option<User> instead of undefined), a five-step refactoring guide, and converting between Option and Either via E.fromOption. Converting callbacks to Task rewrites Node-style callback-hell file processing (including a manual counter-based "wait for N callbacks" pattern) into TaskEither-wrapped fs/promises calls composed with TE.map/TE.flatMap, plus parallel vs. sequential batch processing via A.traverse(TE.ApplicativePar)/ApplicativeSeq, and a generic fromCallback converter for legacy Node-style APIs.
Converting class-based DI to Reader rewrites a constructor-injected UserService class (logger, repository, email service) into ReaderTaskEither functions sharing an AppEnv interface, built with RTE.ask, composed via pipe/RTE.flatMap, and shows why it's easier to test - a createTestEnv() swaps in in-memory logger/repo/email captures without mocking a class hierarchy. Converting imperative loops covers for loops with multiple side-effecting accumulators rewritten as separate A.filter/A.map/A.reduce passes, then as a single-pass A.foldMap using a hand-defined Monoid; nested loops (dedup across an order/item structure) rewritten via A.flatMap + A.uniq; and while-loop pagination rewritten as tail-recursion (go(cursor, accumulated)) or RA.unfold. Migrating Promise chains to TaskEither rewrites .then()/.catch() chains into pipe+TE.flatMap sequences with TE.filterOrElse replacing mid-chain conditionals, Promise.all replaced by A.traverse(TE.ApplicativePar) (fail-fast) versus a T.Task-based variant that collects both successes and failures via TE.bimap+A.separate, and Promise.race replaced by a typed raceTaskEithers plus TE.orElse-based fallback chains.
Six common pitfalls are called out explicitly: forgetting to actually run a Task (fetchData() is still a Task, not a result - it needs the trailing ()); breaking out of the fp-ts ecosystem by throwing inside an otherwise-typed pipeline instead of using TE.fromEither; using map where flatMap is needed, producing a nested Either; losing original error context in tryCatch's error mapper (fixed with typed error unions); overusing fromNullable/Option for trivial single-step null checks where ?? /?. is simpler; and unsafely casting an Either to its Right branch instead of handling Left.
Gradual adoption strategies include starting fp-ts conversion at system boundaries (API responses, DB results, file I/O, input validation) while leaving internals imperative; building bridge functions (unsafeUnwrap, catchSync, fromPromise, toPromise) to cross between paradigms; migrating module-by-module (internal functions typed first, public API converted once internals are stable); type-driven migration (change a type signature first and let the compiler surface every call site needing an update); and writing behavior-documenting tests for both Right/Left cases.
A full section on when NOT to refactor is explicit: simple synchronous formatting code, performance-critical hot-path loops over large arrays (fp-ts operations create intermediate arrays), code that must match a third-party library's imperative interface (e.g. Express handlers, converting only at the boundary), code touched by team members unfamiliar with fp-ts, trivial one-off null checks, cases where the error type genuinely doesn't matter (you're going to throw/log either way), and test code (which should stay readable over being maximally functional). A quick-reference table maps roughly 17 imperative patterns (try/catch, throw, ??, ?., array methods, Promise methods, new Class(deps), for/while) to their fp-ts equivalents, and the summary lands on six principles: start small, be pragmatic, let types drive the refactor, test each conversion, document team-specific patterns, and only add the complexity where it earns its keep - the goal is more maintainable, type-safe code, not functional programming for its own sake.
When to use - and when NOT to
Use this skill when refactoring an existing imperative TypeScript codebase toward fp-ts patterns, for tasks converting try/catch, null checks, callbacks, DI, or loops into functional equivalents, or when migration guidance and tradeoffs are needed rather than isolated fp-ts examples. Do not treat the output as a substitute for environment-specific validation, testing, or expert review, and stop to ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
Who it's for
TypeScript teams migrating an existing imperative codebase to fp-ts who need concrete before/after conversions, common pitfalls to avoid, and - just as importantly - honest guidance on where fp-ts isn't worth the added complexity.
FAQ
Common questions
Discussion
Questions & comments ยท 0
Sign In Sign in to leave a comment.