| name | fp-refactor |
| description | Comprehensive guide for refactoring imperative TypeScript code to fp-ts functional patterns |
| category | Document Processing |
| source | antigravity |
| tags | ["typescript","node","api","ai","document","rag"] |
| url | https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/fp-refactor |
Refactoring Imperative Code to fp-ts
This skill provides comprehensive patterns and strategies for migrating existing imperative TypeScript code to fp-ts functional programming patterns.
When to Use
- You are refactoring an existing imperative TypeScript codebase toward fp-ts patterns.
- The task involves converting
try/catch, null checks, callbacks, DI, or loops into functional equivalents.
- You need migration guidance and tradeoffs, not just isolated fp-ts examples.
Table of Contents
- Converting try-catch to Either/TaskEither
- Converting null checks to Option
- Converting callbacks to Task
- Converting class-based DI to Reader
- Converting imperative loops to functional operations
- Migrating Promise chains to TaskEither
- Common Pitfalls
- Gradual Adoption Strategies
- When NOT to Refactor
1. Converting try-catch to Either/TaskEither
The Problem with try-catch
Traditional try-catch blocks have several issues:
- Error handling is implicit and easy to forget
- The type system doesn't track which functions can throw
- Control flow is non-linear and harder to reason about
- Composing multiple fallible operations is verbose
Pattern: Synchronous try-catch to Either
Before (Imperative)
function parseJSON(input: string): unknown {
try {
return JSON.parse(input);
} catch (error) {
throw new Error(`Invalid JSON: ${error}`);
}
}
function validateUser(data: unknown): User {
try {
if (!data || typeof data !== 'object') {
throw new Error('Data must be an object');
}
const obj = data as Record<string, unknown>;
if (typeof obj.name !== 'string') {
throw new Error('Name is required');
}
if (typeof obj.age !== 'number') {
throw new Error('Age must be a number');
}
return { name: obj.name, : obj. };
} (error) {
error;
}
}
(): | {
{
data = (input);
user = (data);
user;
} (error) {
.(, error);
;
}
}
After (fp-ts Either)
import * as E from 'fp-ts/Either';
import * as J from 'fp-ts/Json';
import { pipe } from 'fp-ts/function';
interface User {
name: string;
age: number;
}
const parseJSON = (input: string): E.Either<Error, unknown> =>
pipe(
J.parse(input),
E.mapLeft((e) => new Error(`Invalid JSON: ${e}`))
);
const validateUser = (data: unknown): E.Either<Error, User> => {
if (!data || typeof data !== 'object') {
return E.left(new Error('Data must be an object'));
}
const obj = data as Record<string, >;
( obj. !== ) {
E.( ());
}
( obj. !== ) {
E.( ());
}
E.({ : obj., : obj. });
};
processUserInput = (: ): E.<, > =>
(
(input),
E.(validateUser)
);
(
(),
E.(
.(, error.),
.(, user)
)
);
Step-by-Step Refactoring Guide
- Identify the error type: Determine what errors can occur and create appropriate error types
- Change return type: From
T to Either<E, T> where E is your error type
- Replace throw statements: Convert
throw new Error(...) to E.left(new Error(...))
- Replace return statements: Convert
return value to E.right(value)
- Remove try-catch blocks: They're no longer needed
- Update callers: Use
pipe with E.flatMap to chain operations
Pattern: Async try-catch to TaskEither
Before (Imperative)
async function fetchUser(id: string): Promise<User> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
return validateUser(data);
} catch (error) {
throw new Error(`Failed to fetch user: ${error}`);
}
}
async function fetchUserPosts(userId: string): Promise<Post[]> {
try {
const response = await fetch(`/api/users/${userId}/posts`);
if (!response.ok) {