
About
Comprehensive guide for refactoring imperative TypeScript code to fp-ts functional patterns
name: fp-refactor description: Comprehensive guide for refactoring imperative TypeScript code to fp-ts functional patterns risk: unknown source: community version: 1.0.0 author: fp-ts-skills tags:
- fp-ts
- refactoring
- functional-programming
- typescript
- migration
- either
- option
- task
- reader
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, age: obj.age };
} catch (error) {
throw error;
}
}
// Usage with nested try-catch
function processUserInput(input: string): User | null {
try {
const data = parseJSON(input);
const user = validateUser(data);
return user;
} catch (error) {
console.error('Failed to process user:', error);
return null;
}
}
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;
}
// Use Json.parse which returns Either<Error, Json>
const parseJSON = (input: string): E.Either<Error, unknown> =>
pipe(
J.parse(input),
E.mapLeft((e) => new Error(`Invalid JSON: ${e}`))
);
// Validation returns Either, making errors explicit in types
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, unknown>;
if (typeof obj.name !== 'string') {
return E.left(new Error('Name is required'));
}
if (typeof obj.age !== 'number') {
return E.left(new Error('Age must be a number'));
}
return E.right({ name: obj.name, age: obj.age });
};
// Compose with pipe and flatMap - errors propagate automatically
const processUserInput = (input: string): E.Either<Error, User> =>
pipe(
parseJSON(input),
E.flatMap(validateUser)
);
// Handle both cases explicitly
pipe(
processUserInput('{"name": "Alice", "age": 30}'),
E.match(
(error) => console.error('Failed to process user:', error.message),
(user) => console.log('User:', user)
)
);
Step-by-Step Refactoring Guide
- Identify the error type: Determine what errors can occur and create appropriate error types
- Change return type: From
TtoEither<E, T>whereEis your error type - Replace throw statements: Convert
throw new Error(...)toE.left(new Error(...)) - Replace return statements: Convert
return valuetoE.right(value) - Remove try-catch blocks: They're no longer needed
- Update callers: Use
pipewithE.flatMapto 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
Compatible Tools
Claude CodeCursor
Tags
Frontend