Pragmatic Functional Programming by whatiskadudoing/fp-ts-skills
npx skills add https://github.com/whatiskadudoing/fp-ts-skills --skill 'Pragmatic Functional Programming'先读这个。 本指南抛开学术术语,向你展示真正重要的内容。没有范畴论。没有抽象废话。只有能让你的代码变得更好的模式。
如果函数式编程让你的代码更难读,就不要用它。
FP 是一种工具,不是宗教。在它有帮助时使用它。在它没有帮助时跳过它。
这五种模式能给你带来大部分好处。在探索其他内容之前,先掌握这些。
与其嵌套函数调用或创建中间变量,不如按阅读顺序链式操作。
import { pipe } from 'fp-ts/function'
// 之前:难以阅读(由内向外)
const result = format(validate(parse(input)))
// 之前:变量太多
const parsed = parse(input)
const validated = validate(parsed)
const result = format(validated)
// 之后:清晰、线性的流程
const result = pipe(
input,
parse,
validate,
format
)
何时使用 pipe:
何时跳过 pipe:
别再到处写 if (x !== null && x !== undefined) 了。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
import * as O from 'fp-ts/Option'
import { pipe } from 'fp-ts/function'
// 之前:防御性的 null 检查
function getUserCity(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
}
// 之后:链式处理潜在的缺失值
const 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')
)
通俗翻译:
O.fromNullable(x) = "包装这个值,将 null/undefined 视为 '无'"O.flatMap(fn) = "如果有值,应用这个函数"O.getOrElse(() => default) = "解包,如果无值则使用这个默认值"停止为预期的失败抛出异常。将错误作为值返回。
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
// 之前:隐藏的失败模式
function parseAge(input: string): number {
const age = parseInt(input, 10)
if (isNaN(age)) throw new Error('Invalid age')
if (age < 0) throw new Error('Age cannot be negative')
return age
}
// 之后:错误在类型中可见
function parseAge(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)
}
// 使用它
const result = parseAge(userInput)
if (E.isRight(result)) {
console.log(`Age is ${result.right}`)
} else {
console.log(`Error: ${result.left}`)
}
通俗翻译:
E.right(value) = "成功,返回这个值"E.left(error) = "失败,返回这个错误"E.isRight(x) = "成功了吗?"在容器内转换值,而无需先提取它们。
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'
// 在 Option 内转换
const maybeUser: O.Option<User> = O.some({ name: 'Alice', age: 30 })
const maybeName: O.Option<string> = pipe(
maybeUser,
O.map(user => user.name)
)
// 在 Either 内转换
const result: E.Either<Error, number> = E.right(5)
const doubled: E.Either<Error, number> = pipe(
result,
E.map(n => n * 2)
)
// 转换数组(同样的概念!)
const numbers = [1, 2, 3]
const doubled = pipe(
numbers,
A.map(n => n * 2)
)
当每一步都可能失败时,将它们链式连接起来。
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')
// 链式处理所有步骤 - 如果任何一步失败,整个过程失败
const getValidEmail = (input: string): E.Either<string, string> =>
pipe(
parseJSON(input),
E.flatMap(extractEmail),
E.flatMap(validateEmail)
)
// 成功路径:Right('user@example.com')
// 任何失败:Left('specific error message')
通俗解释: flatMap 意思是"如果这一步成功了,尝试下一步"
函数式编程并不总是答案。以下情况请保持简单。
// 直接使用可选链 - 这是语言内置的
const city = user?.address?.city ?? 'Unknown'
// 不要过度复杂化
const city = pipe(
O.fromNullable(user),
O.flatMap(u => O.fromNullable(u.address)),
O.flatMap(a => O.fromNullable(a.city)),
O.getOrElse(() => 'Unknown')
)
// 当需要提前退出或复杂逻辑时,for 循环没问题
function findFirst(items: Item[], predicate: (i: Item) => boolean): Item | null {
for (const item of items) {
if (predicate(item)) return item
}
return null
}
// 当没有帮助时,不要强迫使用 FP
const result = pipe(
items,
A.findFirst(predicate),
O.toNullable
)
// 对于热点路径,命令式更快(没有中间数组)
function sumLarge(numbers: number[]): number {
let sum = 0
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i]
}
return sum
}
// fp-ts 会创建中间结构
const sum = pipe(numbers, A.reduce(0, (acc, n) => acc + n))
如果只有你能读懂代码,那就不是好代码。
// 如果你的团队了解这种模式
async function getUser(id: string): Promise<User | null> {
try {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) return null
return await response.json()
} catch {
return null
}
}
// 不要强迫他们使用这个
const 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(new Error('Not found'))),
TE.flatMap(r => TE.tryCatch(() => r.json(), E.toError))
)
// 之前:嵌套三元表达式噩梦
const message = user === null
? 'No user'
: user.isAdmin
? `Admin: ${user.name}`
: `User: ${user.name}`
// 之后:清晰的情况处理
const message = pipe(
O.fromNullable(user),
O.fold(
() => 'No user',
(u) => u.isAdmin ? `Admin: ${u.name}` : `User: ${u.name}`
)
)
// 之前:到处是 try-catch
let config
try {
config = JSON.parse(rawConfig)
} catch {
config = defaultConfig
}
// 之后:一行代码
const config = pipe(
E.tryCatch(() => JSON.parse(rawConfig), () => 'parse error'),
E.getOrElse(() => defaultConfig)
)
// 之前:调用者可能忘记检查
function findUser(id: string): User | undefined {
return users.find(u => u.id === id)
}
// 之后:类型强制调用者处理缺失情况
function findUser(id: string): O.Option<User> {
return O.fromNullable(users.find(u => u.id === id))
}
// 之前:只是字符串
function validate(data: unknown): E.Either<string, User> {
// ...
return E.left('validation failed')
}
// 之后:结构化的错误
type ValidationError = {
field: string
message: string
}
function validate(data: unknown): E.Either<ValidationError, User> {
// ...
return E.left({ field: 'email', message: 'Invalid format' })
}
// 无需类即可创建特定的错误类型
const NotFound = (id: string) => ({ _tag: 'NotFound' as const, id })
const Unauthorized = { _tag: 'Unauthorized' as const }
const ValidationFailed = (errors: string[]) =>
({ _tag: 'ValidationFailed' as const, errors })
type AppError =
| ReturnType<typeof NotFound>
| typeof Unauthorized
| ReturnType<typeof ValidationFailed>
// 现在你可以进行模式匹配
const handleError = (error: AppError): string => {
switch (error._tag) {
case 'NotFound': return `Item ${error.id} not found`
case 'Unauthorized': return 'Please log in'
case 'ValidationFailed': return error.errors.join(', ')
}
}
// 之前
fetchUser(id, (user) => {
if (!user) return handleNoUser()
fetchPosts(user.id, (posts) => {
if (!posts) return handleNoPosts()
fetchComments(posts[0].id, (comments) => {
render(user, posts, comments)
})
})
})
// 之后(使用 TaskEither 处理异步)
import * as TE from 'fp-ts/TaskEither'
const loadData = (id: string) =>
pipe(
fetchUser(id),
TE.flatMap(user => pipe(
fetchPosts(user.id),
TE.map(posts => ({ user, posts }))
)),
TE.flatMap(({ user, posts }) => pipe(
fetchComments(posts[0].id),
TE.map(comments => ({ user, posts, comments }))
))
)
// 执行
const result = await loadData('123')()
pipe(
result,
E.fold(handleError, ({ user, posts, comments }) => render(user, posts, comments))
)
// 之前
function getManagerEmail(employee: Employee): string | null {
if (!employee.department) return null
if (!employee.department.manager) return null
if (!employee.department.manager.email) return null
return employee.department.manager.email
}
// 之后
const getManagerEmail = (employee: Employee): O.Option<string> =>
pipe(
O.fromNullable(employee.department),
O.flatMap(d => O.fromNullable(d.manager)),
O.flatMap(m => O.fromNullable(m.email))
)
// 使用它
pipe(
getManagerEmail(employee),
O.fold(
() => sendToDefault(),
(email) => sendTo(email)
)
)
// 之前:在第一个错误处抛出
function validateUser(data: unknown): User {
if (!data || typeof data !== 'object') throw new Error('Must be object')
const obj = data as Record<string, unknown>
if (typeof obj.email !== 'string') throw new Error('Email required')
if (!obj.email.includes('@')) throw new Error('Invalid email')
if (typeof obj.age !== 'number') throw new Error('Age required')
if (obj.age < 0) throw new Error('Age must be positive')
return obj as User
}
// 之后:返回第一个错误,类型安全
const validateUser = (data: unknown): E.Either<string, User> =>
pipe(
E.Do,
E.bind('obj', () =>
typeof data === 'object' && data !== null
? E.right(data as Record<string, unknown>)
: E.left('Must be object')
),
E.bind('email', ({ obj }) =>
typeof obj.email === 'string' && obj.email.includes('@')
? E.right(obj.email)
: E.left('Valid email required')
),
E.bind('age', ({ obj }) =>
typeof obj.age === 'number' && obj.age >= 0
? E.right(obj.age)
: E.left('Valid age required')
),
E.map(({ email, age }) => ({ email, age }))
)
// 之前
async function processOrder(orderId: string): Promise<Receipt> {
const order = await fetchOrder(orderId)
if (!order) throw new Error('Order not found')
const validated = await validateOrder(order)
if (!validated.success) throw new Error(validated.error)
const payment = await processPayment(validated.order)
if (!payment.success) throw new Error('Payment failed')
return generateReceipt(payment)
}
// 之后
const processOrder = (orderId: string): TE.TaskEither<string, Receipt> =>
pipe(
fetchOrderTE(orderId),
TE.flatMap(order =>
order ? TE.right(order) : TE.left('Order not found')
),
TE.flatMap(validateOrderTE),
TE.flatMap(processPaymentTE),
TE.map(generateReceipt)
)
在使用任何 FP 模式之前,问自己:"初级开发人员能理解这个吗?"
const result = pipe(
data,
A.filter(flow(prop('status'), equals('active'))),
A.map(flow(prop('value'), multiply(2))),
A.reduce(monoid.concat, monoid.empty),
O.fromPredicate(gt(threshold))
)
const activeItems = data.filter(item => item.status === 'active')
const doubledValues = activeItems.map(item => item.value * 2)
const total = doubledValues.reduce((sum, val) => sum + val, 0)
const result = total > threshold ? O.some(total) : O.none
const result = pipe(
data,
A.filter(item => item.status === 'active'),
A.map(item => item.value * 2),
A.reduce(0, (sum, val) => sum + val),
total => total > threshold ? O.some(total) : O.none
)
| 你想要什么 | 通俗解释 | fp-ts |
|---|---|---|
| 处理 null/undefined | "包装这个可为空的值" | O.fromNullable(x) |
| 缺失时的默认值 | "如果无值,使用这个" | O.getOrElse(() => default) |
| 存在时转换 | "如果有值,改变它" | O.map(fn) |
| 链式处理可为空的操作 | "如果有值,尝试这个" | O.flatMap(fn) |
| 返回成功 | "成功,这是值" | E.right(value) |
| 返回失败 | "失败,这是原因" | E.left(error) |
| 包装可能抛出异常的函数 | "尝试这个,捕获错误" | E.tryCatch(fn, onError) |
| 处理两种情况 | "错误时做这个,成功时做那个" | E.fold(onLeft, onRight) |
| 链式操作 | "然后做这个,然后做那个" | pipe(x, fn1, fn2, fn3) |
一旦熟悉了这些模式,可以探索:
但不要急于求成。这里的基础知识将处理 80% 的实际场景。在向你的工具箱添加更多工具之前,先熟悉这些。
每周安装次数
–
代码仓库
GitHub 星标数
4
首次出现
–
安全审计
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.
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.
These five patterns give you most of the benefits. Master these before exploring anything else.
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 variables
const parsed = parse(input)
const validated = validate(parsed)
const result = format(validated)
// After: Clear, linear flow
const result = pipe(
input,
parse,
validate,
format
)
When to use pipe:
When to skip pipe:
Stop writing if (x !== null && x !== undefined) everywhere.
import * as O from 'fp-ts/Option'
import { pipe } from 'fp-ts/function'
// Before: Defensive null checking
function getUserCity(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 values
const 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"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 mode
function parseAge(input: string): number {
const age = parseInt(input, 10)
if (isNaN(age)) throw new Error('Invalid age')
if (age < 0) throw new Error('Age cannot be negative')
return age
}
// After: Errors are visible in the type
function parseAge(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 it
const 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?"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 Option
const maybeUser: O.Option<User> = O.some({ name: 'Alice', age: 30 })
const maybeName: O.Option<string> = pipe(
maybeUser,
O.map(user => user.name)
)
// Transform inside Either
const result: E.Either<Error, number> = E.right(5)
const doubled: 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)
)
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 fails
const 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"
Functional programming is not always the answer. Here's when to keep it simple.
// Just use optional chaining - it's built into the language
const city = user?.address?.city ?? 'Unknown'
// DON'T overcomplicate it
const city = pipe(
O.fromNullable(user),
O.flatMap(u => O.fromNullable(u.address)),
O.flatMap(a => O.fromNullable(a.city)),
O.getOrElse(() => 'Unknown')
)
// A for loop is fine when you need early exit or complex logic
function findFirst(items: Item[], predicate: (i: Item) => boolean): Item | null {
for (const item of items) {
if (predicate(item)) return item
}
return null
}
// DON'T force FP when it doesn't help
const result = pipe(
items,
A.findFirst(predicate),
O.toNullable
)
// For hot paths, imperative is faster (no intermediate arrays)
function sumLarge(numbers: number[]): number {
let sum = 0
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i]
}
return sum
}
// fp-ts creates intermediate structures
const sum = pipe(numbers, A.reduce(0, (acc, n) => acc + n))
If you're the only one who can read the code, it's not good code.
// If your team knows this pattern
async function getUser(id: string): Promise<User | null> {
try {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) return null
return await response.json()
} catch {
return null
}
}
// Don't force this on them
const 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(new Error('Not found'))),
TE.flatMap(r => TE.tryCatch(() => r.json(), E.toError))
)
// Before: Nested ternary nightmare
const message = user === null
? 'No user'
: user.isAdmin
? `Admin: ${user.name}`
: `User: ${user.name}`
// After: Clear case handling
const message = pipe(
O.fromNullable(user),
O.fold(
() => 'No user',
(u) => u.isAdmin ? `Admin: ${u.name}` : `User: ${u.name}`
)
)
// Before: try-catch everywhere
let config
try {
config = JSON.parse(rawConfig)
} catch {
config = defaultConfig
}
// After: One-liner
const config = pipe(
E.tryCatch(() => JSON.parse(rawConfig), () => 'parse error'),
E.getOrElse(() => defaultConfig)
)
// Before: Caller might forget to check
function findUser(id: string): User | undefined {
return users.find(u => u.id === id)
}
// After: Type forces caller to handle missing case
function findUser(id: string): O.Option<User> {
return O.fromNullable(users.find(u => u.id === id))
}
// Before: Just strings
function validate(data: unknown): E.Either<string, User> {
// ...
return E.left('validation failed')
}
// After: Structured errors
type ValidationError = {
field: string
message: string
}
function validate(data: unknown): E.Either<ValidationError, User> {
// ...
return E.left({ field: 'email', message: 'Invalid format' })
}
// Create specific error types without classes
const NotFound = (id: string) => ({ _tag: 'NotFound' as const, id })
const Unauthorized = { _tag: 'Unauthorized' as const }
const ValidationFailed = (errors: string[]) =>
({ _tag: 'ValidationFailed' as const, errors })
type AppError =
| ReturnType<typeof NotFound>
| typeof Unauthorized
| ReturnType<typeof ValidationFailed>
// Now you can pattern match
const handleError = (error: AppError): string => {
switch (error._tag) {
case 'NotFound': return `Item ${error.id} not found`
case 'Unauthorized': return 'Please log in'
case 'ValidationFailed': return error.errors.join(', ')
}
}
// Before
fetchUser(id, (user) => {
if (!user) return handleNoUser()
fetchPosts(user.id, (posts) => {
if (!posts) return handleNoPosts()
fetchComments(posts[0].id, (comments) => {
render(user, posts, comments)
})
})
})
// After (with TaskEither for async)
import * as TE from 'fp-ts/TaskEither'
const loadData = (id: string) =>
pipe(
fetchUser(id),
TE.flatMap(user => pipe(
fetchPosts(user.id),
TE.map(posts => ({ user, posts }))
)),
TE.flatMap(({ user, posts }) => pipe(
fetchComments(posts[0].id),
TE.map(comments => ({ user, posts, comments }))
))
)
// Execute
const result = await loadData('123')()
pipe(
result,
E.fold(handleError, ({ user, posts, comments }) => render(user, posts, comments))
)
// Before
function getManagerEmail(employee: Employee): string | null {
if (!employee.department) return null
if (!employee.department.manager) return null
if (!employee.department.manager.email) return null
return employee.department.manager.email
}
// After
const getManagerEmail = (employee: Employee): O.Option<string> =>
pipe(
O.fromNullable(employee.department),
O.flatMap(d => O.fromNullable(d.manager)),
O.flatMap(m => O.fromNullable(m.email))
)
// Use it
pipe(
getManagerEmail(employee),
O.fold(
() => sendToDefault(),
(email) => sendTo(email)
)
)
// Before: Throws on first error
function validateUser(data: unknown): User {
if (!data || typeof data !== 'object') throw new Error('Must be object')
const obj = data as Record<string, unknown>
if (typeof obj.email !== 'string') throw new Error('Email required')
if (!obj.email.includes('@')) throw new Error('Invalid email')
if (typeof obj.age !== 'number') throw new Error('Age required')
if (obj.age < 0) throw new Error('Age must be positive')
return obj as User
}
// After: Returns first error, type-safe
const validateUser = (data: unknown): E.Either<string, User> =>
pipe(
E.Do,
E.bind('obj', () =>
typeof data === 'object' && data !== null
? E.right(data as Record<string, unknown>)
: E.left('Must be object')
),
E.bind('email', ({ obj }) =>
typeof obj.email === 'string' && obj.email.includes('@')
? E.right(obj.email)
: E.left('Valid email required')
),
E.bind('age', ({ obj }) =>
typeof obj.age === 'number' && obj.age >= 0
? E.right(obj.age)
: E.left('Valid age required')
),
E.map(({ email, age }) => ({ email, age }))
)
// Before
async function processOrder(orderId: string): Promise<Receipt> {
const order = await fetchOrder(orderId)
if (!order) throw new Error('Order not found')
const validated = await validateOrder(order)
if (!validated.success) throw new Error(validated.error)
const payment = await processPayment(validated.order)
if (!payment.success) throw new Error('Payment failed')
return generateReceipt(payment)
}
// After
const processOrder = (orderId: string): TE.TaskEither<string, Receipt> =>
pipe(
fetchOrderTE(orderId),
TE.flatMap(order =>
order ? TE.right(order) : TE.left('Order not found')
),
TE.flatMap(validateOrderTE),
TE.flatMap(processPaymentTE),
TE.map(generateReceipt)
)
Before using any FP pattern, ask: "Would a junior developer understand this?"
const result = pipe(
data,
A.filter(flow(prop('status'), equals('active'))),
A.map(flow(prop('value'), multiply(2))),
A.reduce(monoid.concat, monoid.empty),
O.fromPredicate(gt(threshold))
)
const activeItems = data.filter(item => item.status === 'active')
const doubledValues = activeItems.map(item => item.value * 2)
const total = doubledValues.reduce((sum, val) => sum + val, 0)
const result = total > threshold ? O.some(total) : O.none
const result = pipe(
data,
A.filter(item => item.status === 'active'),
A.map(item => item.value * 2),
A.reduce(0, (sum, val) => sum + val),
total => total > threshold ? O.some(total) : O.none
)
| What you want | Plain language | fp-ts |
|---|---|---|
| Handle null/undefined | "Wrap this nullable" | O.fromNullable(x) |
| Default for missing | "Use this if nothing" | O.getOrElse(() => default) |
| Transform if present | "If something, change it" | O.map(fn) |
| Chain nullable operations | "If something, try this" | O.flatMap(fn) |
| Return success | "Worked, here's the value" | E.right(value) |
Once comfortable with these patterns, explore:
But don't rush. The basics here will handle 80% of real-world scenarios. Get comfortable with these before adding more tools to your belt.
Weekly Installs
–
Repository
GitHub Stars
4
First Seen
–
Security Audits
Tailwind CSS v4 + shadcn/ui 生产级技术栈配置指南与最佳实践
2,600 周安装
| Return failure | "Failed, here's why" | E.left(error) |
| Wrap throwing function | "Try this, catch errors" | E.tryCatch(fn, onError) |
| Handle both cases | "Do this for error, that for success" | E.fold(onLeft, onRight) |
| Chain operations | "Then do this, then that" | pipe(x, fn1, fn2, fn3) |