Skip to main content

fp-refactor

Comprehensive guide for refactoring imperative TypeScript code to fp-ts functional patterns

Jump to install

Source facts

Repository
muhammedadnank/Antigravity-Skills
Last source activity
June 6, 2026 at 18:30
Detected SKILL.md language
English
Stars
13
Forks
1

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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 1. [Converting try-catch to Either/TaskEither](#1-converting-try-catch-to-eithertaskeither) 2. [Converting null checks to Option](#2-converting-null-checks-to-option) 3. [Converting callbacks to Task](#3-converting-callbacks-to-task) 4. [Converting class-based DI to Reader](#4-converting-class-based-di-to-reader) 5. [Converting imperative loops to functional operations](#5-converting-imperative-loops-to-functional-operations) 6. [Migrating Promise chains to TaskEither](#6-migrating-promise-chains-to-taskeither) 7. [Common Pitfalls](#7-common-pitfalls) 8. [Gradual Adoption Strategies](#8-gradual-adoption-strategies) 9. [When NOT to Refactor](#9-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) ```typescript 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) ```typescript 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 1. **Identify the error type**: Determine what errors can occur and create appropriate error types 2. **Change return type**: From `T` to `Either<E, T>` where `E` is your error type 3. **Replace throw statements**: Convert `throw new Error(...)` to `E.left(new Error(...))` 4. **Replace return statements**: Convert `return value` to `E.right(value)` 5. **Remove try-catch blocks**: They're no longer needed 6. **Update callers**: Use `pipe` with `E.flatMap` to chain operations ### Pattern: Async try-catch to TaskEither #### Before (Imperative) ```typescript 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) { throw new Error(`HTTP error: ${response.status}`); } return await response.json(); } catch (error) { throw new Error(`Failed to fetch posts: ${error}`); } } // Complex orchestration with try-catch async function getUserWithPosts(id: string): Promise<{ user: User; posts: Post[] } | null> { try { const user = await fetchUser(id); const posts = await fetchUserPosts(id); return { user, posts }; } catch (error) { console.error(error); return null; } } ``` #### After (fp-ts TaskEither) ```typescript import * as TE from 'fp-ts/TaskEither'; import * as E from 'fp-ts/Either'; import { pipe } from 'fp-ts/function'; // Wrap fetch in TaskEither const fetchUser = (id: string): TE.TaskEither<Error, User> => pipe( TE.tryCatch( () => fetch(`/api/users/${id}`), (reason) => new Error(`Network error: ${reason}`) ), TE.flatMap((response) => response.ok ? TE.right(response) : TE.left(new Error(`HTTP error: ${response.status}`)) ), TE.flatMap((response) => TE.tryCatch( () => response.json(), (reason) => new Error(`JSON parse error: ${reason}`) ) ), TE.flatMap((data) => TE.fromEither(validateUser(data))) ); const fetchUserPosts = (userId: string): TE.TaskEither<Error, Post[]> => pipe( TE.tryCatch( () => fetch(`/api/users/${userId}/posts`), (reason) => new Error(`Network error: ${reason}`) ), TE.flatMap((response) => response.ok ? TE.right(response) : TE.left(new Error(`HTTP error: ${response.status}`)) ), TE.flatMap((response) => TE.tryCatch( () => response.json(), (reason) => new Error(`JSON parse error: ${reason}`) ) ) ); // Clean composition with automatic error propagation const getUserWithPosts = ( id: string ): TE.TaskEither<Error, { user: User; posts: Post[] }> => pipe( TE.Do, TE.bind('user', () => fetchUser(id)), TE.bind('posts', () => fetchUserPosts(id)) ); // Execute and handle results const main = async () => { const result = await getUserWithPosts('123')(); pipe( result, E.match( (error) => console.error('Failed:', error.message), ({ user, posts }) => console.log('Success:', user, posts) ) ); }; ``` ### Helper: tryCatch Utility Create a reusable wrapper for functions that might throw: ```typescript import * as E from 'fp-ts/Either'; import * as TE from 'fp-ts/TaskEither'; // For sync functions const tryCatchSync = <A>(f: () => A): E.Either<Error, A> => E.tryCatch(f, (e) => (e instanceof Error ? e : new Error(String(e)))); // For async functions const tryCatchAsync = <A>(f: () => Promise<A>): TE.TaskEither<Error, A> => TE.tryCatch(f, (e) => (e instanceof Error ? e : new Error(String(e)))); ``` --- ## 2. Converting null checks to Option ### The Problem with null/undefined - TypeScript's strict null checks help, but null still spreads through code - Chained property access requires verbose null guards - The distinction between "missing" and "present but null" is unclear - Easy to forget null checks leading to runtime errors ### Pattern: Simple null checks to Option #### Before (Imperative) ```typescript interface Config { database?: { host?: string; port?: number; credentials?: { username?: string; password?: string; }; }; } function getDatabaseUrl(config: Config): string | null { if (!config.database) { return null; } if (!config.database.host) { return null; } const port = config.database.port ?? 5432; let auth = ''; if (config.database.credentials) { if (config.database.credentials.username && config.database.credentials.password) { auth = `${config.database.credentials.username}:${config.database.credentials.password}@`; } } return `postgres://${auth}${config.database.host}:${port}`; } // Usage requires null check const url = getDatabaseUrl(config); if (url !== null) { connectToDatabase(url); } else { console.error('Database URL not configured'); } ``` #### After (fp-ts Option) ```typescript import * as O from 'fp-ts/Option'; import { pipe } from 'fp-ts/function'; const getDatabaseUrl = (config: Config): O.Option<string> => pipe( O.fromNullable(config.database), O.flatMap((db) => pipe( O.fromNullable(db.host), O.map((host) => { const port = db.port ?? 5432; const auth = pipe( O.fromNullable(db.credentials), O.flatMap((creds) => pipe( O.Do, O.bind('username', () => O.fromNullable(creds.username)), O.bind('password', () => O.fromNullable(creds.password)), O.map(({ username, password }) => `${username}:${password}@`) ) ), O.getOrElse(() => '') ); return `postgres://${auth}${host}:${port}`; }) ) ) ); // Usage is explicit about the optional nature pipe( getDatabaseUrl(config), O.match( () => console.error('Database URL not configured'), (url) => connectToDatabase(url) ) ); ``` ### Pattern: Array find operations #### Before (Imperative) ```typescript interface User { id: string; name: string; email: string; }
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub