fp-refactor — quality + safety report
In the Skillier index (antigravity__fp-refactor) · scanned 2026-06-03 · engine: builtin+triage
✓ Clean — no heuristic safety flags surfaced.
Heuristic flags from the builtin scanner, which is known to over-flag (it trips on legitimate env-reading integrations, security skills, and library .eval calls). This is NOT an authoritative malicious verdict — re-scan with SkillSpector for the authoritative result. Run the authoritative scan →
📇 This skill is in the Skillier index (curated · deduped · quality-filtered). Install Skillier to route & load it into your AI client.
Quality notes
About this skill
Comprehensive guide for refactoring imperative TypeScript code to fp-ts functional patterns
📄 Read the SKILL.md
---
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;
}
function findUserById(users: User[], id: string): User | undefined {
return users.find((u) => u.id === id);
}
function getUserEmail(users: User[], id: string): string | null {
const user = findUserById(users, id);
if (!user) {
return null;
}
return user.email;
}
// Chained lookups get messy
function getManagerEmail(users: User[], employee: { managerId?: string }): string | null {
if (!employee.managerId) {
return null;
}
const manager = findUserById(users, employee.managerId);
if (!manager) {
return null;
}
return manager.email;
}
```
#### After (fp-ts Option)
```typescript
import * as O from 'fp-ts/Option';
import * as A from 'fp-ts/Array';
import { pipe } from 'fp-ts/function';
const findUserById = (users: User[], id: string): O.Option<User> =>
A.findFirst<User>((u) => u.id === id)(users);
const getUserEmail = (users: User[], id: string): O.Option<string> =>
pipe(
findUserById(users, id),
O.map((user) => user.email)
);
const getManagerEmail = (
users: User[],
employee: { managerId?: string }
): O.Option<string> =>
pipe(
O.fromNullable(employee.managerId),
O.flatMap((managerId) => findUserById(users, managerId)),
O.map((manager) => manager.email)
);
```
### Step-by-Step Refactoring Guide
1. **Identify nullable values**: Find all `T | null`, `T | undefin
… (truncated)Want a live grade + an embeddable README badge? Run your skill through the free scanner.
Graded independently by Skillproof — nothing to sell the author. Quality is mechanical + corpus-grounded; safety flags are heuristic (builtin+triage), not a malicious verdict.