A practical, jargon-free guide to fp-ts functional programming - the 80/20 approach that gets results without the academic overhead. Use when writing TypeScript with fp-ts library.
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
A practical, jargon-free guide to fp-ts functional programming - the 80/20 approach that gets results without the academic overhead. Use when writing TypeScript with fp-ts library.
Read this first. This guide cuts through the academic jargon and shows you what actually matters. No category theory. No abstract nonsense. Just patterns that make your code better.
When to Use This Skill
When starting with fp-ts and need practical guidance
When writing TypeScript code that handles nullable values, errors, or async operations
When you want cleaner, more maintainable functional code without the academic overhead
When refactoring imperative code to functional style
The Golden Rule
If functional programming makes your code harder to read, don't use it.
FP is a tool, not a religion. Use it when it helps. Skip it when it doesn't.
The 80/20 of FP
These five patterns give you most of the benefits. Master these before exploring anything else.
1. Pipe: Chain Operations Clearly
Instead of nesting function calls or creating intermediate variables, chain operations in reading order.
import { pipe } from'fp-ts/function'// Before: Hard to read (inside-out)const result = format(validate(parse(input)))
// Before: Too many variablesconst parsed = parse(input)
const validated = validate(parsed)
const result = format(validated)
// After: Clear, linear flowconst result = pipe(
input,
parse,
validate,
format
)
When to use pipe:
3+ transformations on the same data
You find yourself naming throwaway variables
Logic reads better top-to-bottom
When to skip pipe:
Just 1-2 operations (direct call is fine)
The operations don't naturally chain
2. Option: Handle Missing Values Without null Checks
Stop writing if (x !== null && x !== undefined) everywhere.
import * as O from'fp-ts/Option'import { pipe } from'fp-ts/function'// Before: Defensive null checkingfunctiongetUserCity(user: User | null): string {
if (user === null) return'Unknown'if (user.address === null) return'Unknown'if (user.address.city === null) return'Unknown'return user.address.city
}
// After: Chain through potential missing valuesconst getUserCity = (user: User | null): string =>pipe(
O.fromNullable(user),
O.flatMap(u => O.fromNullable(u.address)),
O.flatMap(a => O.fromNullable(a.city)),
O.getOrElse(() =>'Unknown')
)
Plain language translation:
O.fromNullable(x) = "wrap this value, treating null/undefined as 'nothing'"
O.flatMap(fn) = "if we have something, apply this function"
O.getOrElse(() => default) = "unwrap, or use this default if nothing"
3. Either: Make Errors Explicit
Stop throwing exceptions for expected failures. Return errors as values.
import * as E from'fp-ts/Either'import { pipe } from'fp-ts/function'// Before: Hidden failure modefunctionparseAge(input: string): number {
const age = parseInt(input, 10)
if (isNaN(age)) thrownewError('Invalid age')
if (age < 0) thrownewError('Age cannot be negative')
return age
}
// After: Errors are visible in the typefunctionparseAge(input: string): E.Either<string, number> {
const age = parseInt(input, 10)
if (isNaN(age)) return E.left('Invalid age')
if (age < 0) return E.left('Age cannot be negative')
return E.right(age)
}
// Using itconst result = parseAge(userInput)
if (E.isRight(result)) {
console.log(`Age is ${result.right}`)
} else {
console.log(`Error: ${result.left}`)
}
Plain language translation:
E.right(value) = "success with this value"
E.left(error) = "failure with this error"
E.isRight(x) = "did it succeed?"
4. Map: Transform Without Unpacking
Transform values inside containers without extracting them first.
import * as O from'fp-ts/Option'import * as E from'fp-ts/Either'import * as A from'fp-ts/Array'import { pipe } from'fp-ts/function'// Transform inside OptionconstmaybeUser: O.Option<User> = O.some({ name: 'Alice', age: 30 })
constmaybeName: O.Option<string> = pipe(
maybeUser,
O.map(user => user.name)
)
// Transform inside Eitherconstresult: E.Either<Error, number> = E.right(5)
constdoubled: E.Either<Error, number> = pipe(
result,
E.map(n => n * 2)
)
// Transform arrays (same concept!)const numbers = [1, 2, 3]
const doubled = pipe(
numbers,
A.map(n => n * 2)
)
5. FlatMap: Chain Operations That Might Fail
When each step might fail, chain them together.
import * as E from'fp-ts/Either'import { pipe } from'fp-ts/function'const parseJSON = (s: string): E.Either<string, unknown> =>
E.tryCatch(() =>JSON.parse(s), () =>'Invalid JSON')
const extractEmail = (data: unknown): E.Either<string, string> => {
if (typeof data === 'object' && data !== null && 'email'in data) {
return E.right((data as { email: string }).email)
}
return E.left('No email field')
}
const validateEmail = (email: string): E.Either<string, string> =>
email.includes('@') ? E.right(email) : E.left('Invalid email format')
// Chain all steps - if any fails, the whole thing failsconst getValidEmail = (input: string): E.Either<string, string> =>
pipe(
parseJSON(input),
E.flatMap(extractEmail),
E.flatMap(validateEmail)
)
// Success path: Right('user@example.com')// Any failure: Left('specific error message')
Plain language:flatMap means "if this succeeded, try the next thing"
When NOT to Use FP
Functional programming is not always the answer. Here's when to keep it simple.
Simple Null Checks
// Just use optional chaining - it's built into the languageconst city = user?.address?.city ?? 'Unknown'// DON'T overcomplicate itconst city = pipe(
O.fromNullable(user),
O.flatMap(u => O.fromNullable(u.address)),
O.flatMap(a => O.fromNullable(a.city)),
O.getOrElse(() =>'Unknown')
)
Simple Loops
// A for loop is fine when you need early exit or complex logicfunctionfindFirst(items: Item[], predicate: (i: Item) => boolean): Item | null {
for (const item of items) {
if (predicate(item)) return item
}
returnnull
}
// DON'T force FP when it doesn't helpconst result = pipe(
items,
A.findFirst(predicate),
O.toNullable
)
Performance-Critical Code
// For hot paths, imperative is faster (no intermediate arrays)functionsumLarge(numbers: number[]): number {
let sum = 0for (let i = 0; i < numbers.length; i++) {
sum += numbers[i]
}
return sum
}
// fp-ts creates intermediate structuresconst sum = pipe(numbers, A.reduce(0, (acc, n) => acc + n))
When Your Team Doesn't Know FP
If you're the only one who can read the code, it's not good code.
// If your team knows this patternasyncfunctiongetUser(id: string): Promise<User | null> {
try {
const response = awaitfetch(`/api/users/${id}`)
if (!response.ok) returnnullreturnawait response.json()
} catch {
returnnull
}
}
// Don't force this on themconst getUser = (id: string): TE.TaskEither<Error, User> =>
pipe(
TE.tryCatch(() =>fetch(`/api/users/${id}`), E.toError),
TE.flatMap(r => r.ok ? TE.right(r) : TE.left(newError('Not found'))),
TE.flatMap(r =>TE.tryCatch(() => r.json(), E.toError))
)