fp-ts-task-either
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesefp-ts TaskEither Async Patterns
fp-ts TaskEither 异步模式
TaskEither combines the laziness of Task with the error handling of Either, providing a powerful abstraction for async operations that can fail.
TaskEither结合了Task的惰性和Either的错误处理能力,为可能失败的异步操作提供了强大的抽象。
Core Concepts
核心概念
typescript
import * as TE from 'fp-ts/TaskEither'
import * as T from 'fp-ts/Task'
import * as E from 'fp-ts/Either'
import { pipe, flow } from 'fp-ts/function'TaskEither<E, A> is equivalent to
() => Promise<Either<E, A>>- = Error type (left)
E - = Success type (right)
A - Lazy: nothing executes until you call the function
- Composable: chain operations without try/catch
typescript
import * as TE from 'fp-ts/TaskEither'
import * as T from 'fp-ts/Task'
import * as E from 'fp-ts/Either'
import { pipe, flow } from 'fp-ts/function'TaskEither<E, A> 等价于
() => Promise<Either<E, A>>- = 错误类型(左侧)
E - = 成功类型(右侧)
A - 惰性:调用函数前不会执行任何操作
- 可组合:无需try/catch即可链式调用操作
1. Converting Promises to TaskEither
1. 将Promise转换为TaskEither
Using tryCatch
使用tryCatch
The primary way to lift Promises into TaskEither:
typescript
import * as TE from 'fp-ts/TaskEither'
// Basic tryCatch pattern
const fetchUser = (id: string): TE.TaskEither<Error, User> =>
TE.tryCatch(
() => fetch(`/api/users/${id}`).then(res => res.json()),
(reason) => new Error(String(reason))
)
// With typed errors
interface ApiError {
code: string
message: string
status: number
}
const fetchUserTyped = (id: string): TE.TaskEither<ApiError, User> =>
TE.tryCatch(
() => fetch(`/api/users/${id}`).then(res => res.json()),
(reason): ApiError => ({
code: 'FETCH_ERROR',
message: reason instanceof Error ? reason.message : 'Unknown error',
status: 500
})
)将Promise转换为TaskEither的主要方式:
typescript
import * as TE from 'fp-ts/TaskEither'
// 基础tryCatch模式
const fetchUser = (id: string): TE.TaskEither<Error, User> =>
TE.tryCatch(
() => fetch(`/api/users/${id}`).then(res => res.json()),
(reason) => new Error(String(reason))
)
// 带类型的错误
interface ApiError {
code: string
message: string
status: number
}
const fetchUserTyped = (id: string): TE.TaskEither<ApiError, User> =>
TE.tryCatch(
() => fetch(`/api/users/${id}`).then(res => res.json()),
(reason): ApiError => ({
code: 'FETCH_ERROR',
message: reason instanceof Error ? reason.message : 'Unknown error',
status: 500
})
)From existing Either
从已有的Either转换
typescript
// Lift an Either into TaskEither
const fromEither: TE.TaskEither<Error, number> = TE.fromEither(E.right(42))
// From a nullable value
const fromNullable = TE.fromNullable(new Error('Value was null'))
const result = fromNullable(maybeValue) // TaskEither<Error, NonNullable<T>>
// From an Option
import * as O from 'fp-ts/Option'
const fromOption = TE.fromOption(() => new Error('None value'))
const optionResult = fromOption(O.some(42)) // TaskEither<Error, number>typescript
// 将Either转换为TaskEither
const fromEither: TE.TaskEither<Error, number> = TE.fromEither(E.right(42))
// 从可空值转换
const fromNullable = TE.fromNullable(new Error('Value was null'))
const result = fromNullable(maybeValue) // TaskEither<Error, NonNullable<T>>
// 从Option转换
import * as O from 'fp-ts/Option'
const fromOption = TE.fromOption(() => new Error('None value'))
const optionResult = fromOption(O.some(42)) // TaskEither<Error, number>Creating TaskEither values directly
直接创建TaskEither值
typescript
// Success value
const success = TE.right<Error, number>(42)
// Error value
const failure = TE.left<Error, number>(new Error('Something failed'))
// From a predicate
const validatePositive = TE.fromPredicate(
(n: number) => n > 0,
(n) => new Error(`Expected positive, got ${n}`)
)typescript
// 成功值
const success = TE.right<Error, number>(42)
// 错误值
const failure = TE.left<Error, number>(new Error('Something failed'))
// 从断言创建
const validatePositive = TE.fromPredicate(
(n: number) => n > 0,
(n) => new Error(`Expected positive, got ${n}`)
)2. Handling Async Errors Functionally
2. 函数式处理异步错误
Mapping over errors
映射错误
typescript
// Transform the error type
const withMappedError = pipe(
fetchUser('123'),
TE.mapLeft((error) => ({
type: 'USER_FETCH_ERROR' as const,
originalError: error,
timestamp: Date.now()
}))
)
// Bifunctor: map both sides
const mapped = pipe(
fetchUser('123'),
TE.bimap(
(error) => new DetailedError(error), // map error
(user) => user.profile // map success
)
)typescript
// 转换错误类型
const withMappedError = pipe(
fetchUser('123'),
TE.mapLeft((error) => ({
type: 'USER_FETCH_ERROR' as const,
originalError: error,
timestamp: Date.now()
}))
)
// 双函子:同时映射两侧
const mapped = pipe(
fetchUser('123'),
TE.bimap(
(error) => new DetailedError(error), // 映射错误
(user) => user.profile // 映射成功值
)
)Error filtering
错误过滤
typescript
// Filter with error on false
const validateAge = pipe(
fetchUser('123'),
TE.filterOrElse(
(user) => user.age >= 18,
(user) => new Error(`User ${user.name} is underage`)
)
)typescript
// 条件不满足时返回错误
const validateAge = pipe(
fetchUser('123'),
TE.filterOrElse(
(user) => user.age >= 18,
(user) => new Error(`User ${user.name} is underage`)
)
)3. Chaining Async Operations
3. 链式调用异步操作
Sequential chaining with chain/flatMap
使用chain/flatMap进行顺序链式调用
typescript
interface User { id: string; name: string; teamId: string }
interface Team { id: string; name: string; orgId: string }
interface Org { id: string; name: string }
const fetchUser = (id: string): TE.TaskEither<Error, User> =>
TE.tryCatch(() => api.getUser(id), toError)
const fetchTeam = (teamId: string): TE.TaskEither<Error, Team> =>
TE.tryCatch(() => api.getTeam(teamId), toError)
const fetchOrg = (orgId: string): TE.TaskEither<Error, Org> =>
TE.tryCatch(() => api.getOrg(orgId), toError)
// Chain operations sequentially
const getUserOrg = (userId: string): TE.TaskEither<Error, Org> =>
pipe(
fetchUser(userId),
TE.chain((user) => fetchTeam(user.teamId)),
TE.chain((team) => fetchOrg(team.orgId))
)
// flatMap is an alias for chain
const getUserOrgAlt = (userId: string): TE.TaskEither<Error, Org> =>
pipe(
fetchUser(userId),
TE.flatMap((user) => fetchTeam(user.teamId)),
TE.flatMap((team) => fetchOrg(team.orgId))
)typescript
interface User { id: string; name: string; teamId: string }
interface Team { id: string; name: string; orgId: string }
interface Org { id: string; name: string }
const fetchUser = (id: string): TE.TaskEither<Error, User> =>
TE.tryCatch(() => api.getUser(id), toError)
const fetchTeam = (teamId: string): TE.TaskEither<Error, Team> =>
TE.tryCatch(() => api.getTeam(teamId), toError)
const fetchOrg = (orgId: string): TE.TaskEither<Error, Org> =>
TE.tryCatch(() => api.getOrg(orgId), toError)
// 顺序链式调用操作
const getUserOrg = (userId: string): TE.TaskEither<Error, Org> =>
pipe(
fetchUser(userId),
TE.chain((user) => fetchTeam(user.teamId)),
TE.chain((team) => fetchOrg(team.orgId))
)
// flatMap是chain的别名
const getUserOrgAlt = (userId: string): TE.TaskEither<Error, Org> =>
pipe(
fetchUser(userId),
TE.flatMap((user) => fetchTeam(user.teamId)),
TE.flatMap((team) => fetchOrg(team.orgId))
)Chaining with intermediate values
保留中间值的链式调用
typescript
// Use bind to accumulate values
const getFullContext = (userId: string) =>
pipe(
TE.Do,
TE.bind('user', () => fetchUser(userId)),
TE.bind('team', ({ user }) => fetchTeam(user.teamId)),
TE.bind('org', ({ team }) => fetchOrg(team.orgId)),
TE.map(({ user, team, org }) => ({
userName: user.name,
teamName: team.name,
orgName: org.name
}))
)typescript
// 使用bind累积值
const getFullContext = (userId: string) =>
pipe(
TE.Do,
TE.bind('user', () => fetchUser(userId)),
TE.bind('team', ({ user }) => fetchTeam(user.teamId)),
TE.bind('org', ({ team }) => fetchOrg(team.orgId)),
TE.map(({ user, team, org }) => ({
userName: user.name,
teamName: team.name,
orgName: org.name
}))
)4. Parallel vs Sequential Execution
4. 并行与顺序执行
Parallel execution with sequenceArray
使用sequenceArray并行执行
typescript
import * as A from 'fp-ts/Array'
const userIds = ['1', '2', '3', '4', '5']
// Parallel: all requests start immediately
// Fails fast: returns first error encountered
const fetchAllUsersParallel = pipe(
userIds.map(fetchUser),
TE.sequenceArray // TaskEither<Error, readonly User[]>
)
// Sequential: one at a time (use when order matters or rate limiting)
const fetchAllUsersSequential = pipe(
userIds,
A.traverse(TE.ApplicativeSeq)(fetchUser)
)typescript
import * as A from 'fp-ts/Array'
const userIds = ['1', '2', '3', '4', '5']
// 并行:所有请求立即开始
// 快速失败:返回第一个遇到的错误
const fetchAllUsersParallel = pipe(
userIds.map(fetchUser),
TE.sequenceArray // TaskEither<Error, readonly User[]>
)
// 顺序:逐个执行(在顺序重要或需要限流时使用)
const fetchAllUsersSequential = pipe(
userIds,
A.traverse(TE.ApplicativeSeq)(fetchUser)
)Parallel with traverseArray
使用traverseArray并行执行
typescript
// More idiomatic: traverse combines map + sequence
const fetchAllUsers = (ids: string[]): TE.TaskEither<Error, readonly User[]> =>
pipe(ids, TE.traverseArray(fetchUser))
// With index
const fetchWithIndex = pipe(
userIds,
TE.traverseArrayWithIndex((index, id) =>
pipe(
fetchUser(id),
TE.map(user => ({ ...user, index }))
)
)
)typescript
// 更符合习惯:traverse结合了map + sequence
const fetchAllUsers = (ids: string[]): TE.TaskEither<Error, readonly User[]> =>
pipe(ids, TE.traverseArray(fetchUser))
// 带索引
const fetchWithIndex = pipe(
userIds,
TE.traverseArrayWithIndex((index, id) =>
pipe(
fetchUser(id),
TE.map(user => ({ ...user, index }))
)
)
)Collecting all errors vs fail fast
收集所有错误 vs 快速失败
typescript
import * as These from 'fp-ts/These'
import * as TH from 'fp-ts/TaskThese'
// For collecting all errors, consider TaskThese
// Or use validation with sequenceT
import { sequenceT } from 'fp-ts/Apply'
// Parallel execution, collects results
const parallel = sequenceT(TE.ApplyPar)(
fetchUser('1'),
fetchTeam('team-1'),
fetchOrg('org-1')
) // TaskEither<Error, [User, Team, Org]>
// Sequential execution
const sequential = sequenceT(TE.ApplySeq)(
fetchUser('1'),
fetchTeam('team-1'),
fetchOrg('org-1')
)typescript
import * as These from 'fp-ts/These'
import * as TH from 'fp-ts/TaskThese'
// 要收集所有错误,可以考虑TaskThese
// 或者使用sequenceT进行验证
import { sequenceT } from 'fp-ts/Apply'
// 并行执行,收集结果
const parallel = sequenceT(TE.ApplyPar)(
fetchUser('1'),
fetchTeam('team-1'),
fetchOrg('org-1')
) // TaskEither<Error, [User, Team, Org]>
// 顺序执行
const sequential = sequenceT(TE.ApplySeq)(
fetchUser('1'),
fetchTeam('team-1'),
fetchOrg('org-1')
)Concurrent with limit
带限制的并发执行
typescript
// For controlled concurrency, batch your operations
const batchSize = 3
const fetchInBatches = (ids: string[]): TE.TaskEither<Error, User[]> => {
const batches = chunk(ids, batchSize)
return pipe(
batches,
A.traverse(TE.ApplicativeSeq)((batch) =>
pipe(batch, TE.traverseArray(fetchUser))
),
TE.map(A.flatten)
)
}typescript
// 要控制并发,分批处理操作
const batchSize = 3
const fetchInBatches = (ids: string[]): TE.TaskEither<Error, User[]> => {
const batches = chunk(ids, batchSize)
return pipe(
batches,
A.traverse(TE.ApplicativeSeq)((batch) =>
pipe(batch, TE.traverseArray(fetchUser))
),
TE.map(A.flatten)
)
}5. Error Recovery with orElse
5. 使用orElse进行错误恢复
Basic error recovery
基础错误恢复
typescript
// Try primary, fall back to secondary
const fetchWithFallback = pipe(
fetchFromPrimaryApi(id),
TE.orElse((primaryError) =>
pipe(
fetchFromBackupApi(id),
TE.mapLeft((backupError) => ({
primary: primaryError,
backup: backupError
}))
)
)
)
// Recover to a default value
const fetchWithDefault = pipe(
fetchUser(id),
TE.orElse(() => TE.right(defaultUser))
)
// orElseW when recovery has different error type
const fetchWithTypedFallback = pipe(
fetchFromApi(id), // TaskEither<ApiError, User>
TE.orElseW((apiError) => // orElseW allows different error type
fetchFromCache(id) // TaskEither<CacheError, User>
)
) // TaskEither<CacheError, User>typescript
// 先尝试主源,失败则回退到备用源
const fetchWithFallback = pipe(
fetchFromPrimaryApi(id),
TE.orElse((primaryError) =>
pipe(
fetchFromBackupApi(id),
TE.mapLeft((backupError) => ({
primary: primaryError,
backup: backupError
}))
)
)
)
// 恢复为默认值
const fetchWithDefault = pipe(
fetchUser(id),
TE.orElse(() => TE.right(defaultUser))
)
// 当恢复操作的错误类型不同时使用orElseW
const fetchWithTypedFallback = pipe(
fetchFromApi(id), // TaskEither<ApiError, User>
TE.orElseW((apiError) => // orElseW允许不同的错误类型
fetchFromCache(id) // TaskEither<CacheError, User>
)
) // TaskEither<CacheError, User>Retry patterns
重试模式
typescript
const retry = <E, A>(
te: TE.TaskEither<E, A>,
retries: number,
delay: number
): TE.TaskEither<E, A> =>
pipe(
te,
TE.orElse((error) =>
retries > 0
? pipe(
T.delay(delay)(T.of(undefined)),
T.chain(() => retry(te, retries - 1, delay * 2))
)
: TE.left(error)
)
)
// Usage
const fetchWithRetry = retry(fetchUser('123'), 3, 1000)typescript
const retry = <E, A>(
te: TE.TaskEither<E, A>,
retries: number,
delay: number
): TE.TaskEither<E, A> =>
pipe(
te,
TE.orElse((error) =>
retries > 0
? pipe(
T.delay(delay)(T.of(undefined)),
T.chain(() => retry(te, retries - 1, delay * 2))
)
: TE.left(error)
)
)
// 使用示例
const fetchWithRetry = retry(fetchUser('123'), 3, 1000)Conditional recovery
条件恢复
typescript
// Only recover from specific errors
const recoverFromNotFound = pipe(
fetchUser(id),
TE.orElse((error) =>
error.code === 'NOT_FOUND'
? TE.right(createDefaultUser(id))
: TE.left(error) // re-throw other errors
)
)
// Alt: try alternatives in order
import { alt } from 'fp-ts/TaskEither'
const fetchFromAnywhere = pipe(
fetchFromCache(id),
TE.alt(() => fetchFromApi(id)),
TE.alt(() => fetchFromBackup(id))
)typescript
// 仅从特定错误中恢复
const recoverFromNotFound = pipe(
fetchUser(id),
TE.orElse((error) =>
error.code === 'NOT_FOUND'
? TE.right(createDefaultUser(id))
: TE.left(error) // 重新抛出其他错误
)
)
// 备选方案:按顺序尝试多个源
import { alt } from 'fp-ts/TaskEither'
const fetchFromAnywhere = pipe(
fetchFromCache(id),
TE.alt(() => fetchFromApi(id)),
TE.alt(() => fetchFromBackup(id))
)6. Pattern Matching Async Results
6. 异步结果的模式匹配
Using fold/match
使用fold/match
typescript
// fold executes the TaskEither and handles both cases
const handleResult = pipe(
fetchUser('123'),
TE.fold(
(error) => T.of(`Error: ${error.message}`),
(user) => T.of(`Welcome, ${user.name}`)
)
) // Task<string> - no longer has error channel
// match is an alias for fold
const handleWithMatch = pipe(
fetchUser('123'),
TE.match(
(error) => ({ success: false, error }),
(user) => ({ success: true, data: user })
)
)
// matchW when handlers return different types
const handleWithMatchW = pipe(
fetchUser('123'),
TE.matchW(
(error) => ({ type: 'error' as const, error }),
(user) => ({ type: 'success' as const, user })
)
)typescript
// fold执行TaskEither并处理两种情况
const handleResult = pipe(
fetchUser('123'),
TE.fold(
(error) => T.of(`Error: ${error.message}`),
(user) => T.of(`Welcome, ${user.name}`)
)
) // Task<string> - 不再包含错误通道
// match是fold的别名
const handleWithMatch = pipe(
fetchUser('123'),
TE.match(
(error) => ({ success: false, error }),
(user) => ({ success: true, data: user })
)
)
// 当处理函数返回不同类型时使用matchW
const handleWithMatchW = pipe(
fetchUser('123'),
TE.matchW(
(error) => ({ type: 'error' as const, error }),
(user) => ({ type: 'success' as const, user })
)
)Getting the underlying Either
获取底层的Either
typescript
// Execute and get the Either
const getEither = async () => {
const either = await fetchUser('123')()
if (E.isLeft(either)) {
console.error('Failed:', either.left)
} else {
console.log('User:', either.right)
}
}
// Using getOrElse for default
const getWithDefault = pipe(
fetchUser('123'),
TE.getOrElse((error) => T.of(defaultUser))
) // Task<User>
// getOrElseW when default has different type
const getOrNull = pipe(
fetchUser('123'),
TE.getOrElseW(() => T.of(null))
) // Task<User | null>typescript
// 执行并获取Either
const getEither = async () => {
const either = await fetchUser('123')()
if (E.isLeft(either)) {
console.error('Failed:', either.left)
} else {
console.log('User:', either.right)
}
}
// 使用getOrElse获取默认值
const getWithDefault = pipe(
fetchUser('123'),
TE.getOrElse((error) => T.of(defaultUser))
) // Task<User>
// 当默认值类型不同时使用getOrElseW
const getOrNull = pipe(
fetchUser('123'),
TE.getOrElseW(() => T.of(null))
) // Task<User | null>7. Do Notation for Complex Workflows
7. 用于复杂工作流的Do表示法
Building complex operations
构建复杂操作
typescript
interface OrderContext {
user: User
cart: Cart
payment: PaymentMethod
shipping: ShippingAddress
}
const processOrder = (
userId: string,
cartId: string
): TE.TaskEither<OrderError, OrderConfirmation> =>
pipe(
TE.Do,
// Bind values sequentially
TE.bind('user', () => fetchUser(userId)),
TE.bind('cart', () => fetchCart(cartId)),
// Validate intermediate results
TE.filterOrElse(
({ cart }) => cart.items.length > 0,
() => ({ code: 'EMPTY_CART', message: 'Cart is empty' })
),
// Continue building context
TE.bind('payment', ({ user }) => getDefaultPayment(user.id)),
TE.bind('shipping', ({ user }) => getDefaultShipping(user.id)),
// Calculate derived values
TE.bind('total', ({ cart }) => TE.right(calculateTotal(cart))),
// Validate before final operation
TE.filterOrElse(
({ payment, total }) => payment.limit >= total,
({ total }) => ({ code: 'LIMIT_EXCEEDED', message: `Order total ${total} exceeds limit` })
),
// Final operation
TE.chain(({ user, cart, payment, shipping, total }) =>
createOrder({ user, cart, payment, shipping, total })
)
)typescript
interface OrderContext {
user: User
cart: Cart
payment: PaymentMethod
shipping: ShippingAddress
}
const processOrder = (
userId: string,
cartId: string
): TE.TaskEither<OrderError, OrderConfirmation> =>
pipe(
TE.Do,
// 顺序绑定值
TE.bind('user', () => fetchUser(userId)),
TE.bind('cart', () => fetchCart(cartId)),
// 验证中间结果
TE.filterOrElse(
({ cart }) => cart.items.length > 0,
() => ({ code: 'EMPTY_CART', message: 'Cart is empty' })
),
// 继续构建上下文
TE.bind('payment', ({ user }) => getDefaultPayment(user.id)),
TE.bind('shipping', ({ user }) => getDefaultShipping(user.id)),
// 计算派生值
TE.bind('total', ({ cart }) => TE.right(calculateTotal(cart))),
// 最终操作前验证
TE.filterOrElse(
({ payment, total }) => payment.limit >= total,
({ total }) => ({ code: 'LIMIT_EXCEEDED', message: `Order total ${total} exceeds limit` })
),
// 最终操作
TE.chain(({ user, cart, payment, shipping, total }) =>
createOrder({ user, cart, payment, shipping, total })
)
)Parallel fetching within Do
Do表示法中的并行获取
typescript
const getOrderDetails = (orderId: string) =>
pipe(
TE.Do,
TE.bind('order', () => fetchOrder(orderId)),
// Parallel fetch based on order data
TE.bind('details', ({ order }) =>
sequenceT(TE.ApplyPar)(
fetchUser(order.userId),
fetchProducts(order.productIds),
fetchShipping(order.shippingId)
)
),
TE.map(({ order, details: [user, products, shipping] }) => ({
order,
user,
products,
shipping
}))
)typescript
const getOrderDetails = (orderId: string) =>
pipe(
TE.Do,
TE.bind('order', () => fetchOrder(orderId)),
// 根据订单数据并行获取
TE.bind('details', ({ order }) =>
sequenceT(TE.ApplyPar)(
fetchUser(order.userId),
fetchProducts(order.productIds),
fetchShipping(order.shippingId)
)
),
TE.map(({ order, details: [user, products, shipping] }) => ({
order,
user,
products,
shipping
}))
)Using apS for simpler additions
使用apS简化添加操作
typescript
// apS is like bind but doesn't depend on previous values
const enrichUser = (userId: string) =>
pipe(
fetchUser(userId),
TE.bindTo('user'), // Wrap in { user: ... }
TE.apS('config', fetchAppConfig()), // Add independent value
TE.apS('features', fetchFeatureFlags()),
TE.bind('preferences', ({ user }) => fetchPreferences(user.id)) // Dependent
)typescript
// apS类似bind,但不依赖之前的值
const enrichUser = (userId: string) =>
pipe(
fetchUser(userId),
TE.bindTo('user'), // 包装为 { user: ... }
TE.apS('config', fetchAppConfig()), // 添加独立值
TE.apS('features', fetchFeatureFlags()),
TE.bind('preferences', ({ user }) => fetchPreferences(user.id)) // 依赖之前的值
)8. Real-World API Call Patterns
8. 真实场景API调用模式
Typed API client
类型化API客户端
typescript
interface ApiConfig {
baseUrl: string
timeout: number
}
interface ApiError {
code: string
message: string
status: number
details?: unknown
}
const createApiClient = (config: ApiConfig) => {
const request = <T>(
method: string,
path: string,
body?: unknown
): TE.TaskEither<ApiError, T> =>
TE.tryCatch(
async () => {
const response = await fetch(`${config.baseUrl}${path}`, {
method,
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(config.timeout)
})
if (!response.ok) {
const error = await response.json().catch(() => ({}))
throw { status: response.status, ...error }
}
return response.json()
},
(error): ApiError => ({
code: 'API_ERROR',
message: error instanceof Error ? error.message : 'Request failed',
status: (error as any)?.status ?? 500,
details: error
})
)
return {
get: <T>(path: string) => request<T>('GET', path),
post: <T>(path: string, body: unknown) => request<T>('POST', path, body),
put: <T>(path: string, body: unknown) => request<T>('PUT', path, body),
delete: <T>(path: string) => request<T>('DELETE', path)
}
}
// Usage
const api = createApiClient({ baseUrl: '/api', timeout: 5000 })
const getUser = (id: string) => api.get<User>(`/users/${id}`)
const createUser = (data: CreateUserDto) => api.post<User>('/users', data)typescript
interface ApiConfig {
baseUrl: string
timeout: number
}
interface ApiError {
code: string
message: string
status: number
details?: unknown
}
const createApiClient = (config: ApiConfig) => {
const request = <T>(
method: string,
path: string,
body?: unknown
): TE.TaskEither<ApiError, T> =>
TE.tryCatch(
async () => {
const response = await fetch(`${config.baseUrl}${path}`, {
method,
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(config.timeout)
})
if (!response.ok) {
const error = await response.json().catch(() => ({}))
throw { status: response.status, ...error }
}
return response.json()
},
(error): ApiError => ({
code: 'API_ERROR',
message: error instanceof Error ? error.message : 'Request failed',
status: (error as any)?.status ?? 500,
details: error
})
)
return {
get: <T>(path: string) => request<T>('GET', path),
post: <T>(path: string, body: unknown) => request<T>('POST', path, body),
put: <T>(path: string, body: unknown) => request<T>('PUT', path, body),
delete: <T>(path: string) => request<T>('DELETE', path)
}
}
// 使用示例
const api = createApiClient({ baseUrl: '/api', timeout: 5000 })
const getUser = (id: string) => api.get<User>(`/users/${id}`)
const createUser = (data: CreateUserDto) => api.post<User>('/users', data)Request with validation
带验证的请求
typescript
import * as t from 'io-ts'
import { PathReporter } from 'io-ts/PathReporter'
const UserCodec = t.type({
id: t.string,
name: t.string,
email: t.string
})
type User = t.TypeOf<typeof UserCodec>
const fetchAndValidate = <A>(
codec: t.Type<A>,
url: string
): TE.TaskEither<Error, A> =>
pipe(
TE.tryCatch(
() => fetch(url).then(r => r.json()),
(e) => new Error(`Fetch failed: ${e}`)
),
TE.chainEitherK((data) =>
pipe(
codec.decode(data),
E.mapLeft((errors) =>
new Error(`Validation failed: ${PathReporter.report(E.left(errors)).join(', ')}`)
)
)
)
)
const getValidatedUser = (id: string) =>
fetchAndValidate(UserCodec, `/api/users/${id}`)typescript
import * as t from 'io-ts'
import { PathReporter } from 'io-ts/PathReporter'
const UserCodec = t.type({
id: t.string,
name: t.string,
email: t.string
})
type User = t.TypeOf<typeof UserCodec>
const fetchAndValidate = <A>(
codec: t.Type<A>,
url: string
): TE.TaskEither<Error, A> =>
pipe(
TE.tryCatch(
() => fetch(url).then(r => r.json()),
(e) => new Error(`Fetch failed: ${e}`)
),
TE.chainEitherK((data) =>
pipe(
codec.decode(data),
E.mapLeft((errors) =>
new Error(`Validation failed: ${PathReporter.report(E.left(errors)).join(', ')}`)
)
)
)
)
const getValidatedUser = (id: string) =>
fetchAndValidate(UserCodec, `/api/users/${id}`)9. Database Operation Patterns
9. 数据库操作模式
Repository pattern
仓库模式
typescript
interface Repository<E, T, ID> {
findById: (id: ID) => TE.TaskEither<E, T>
findAll: () => TE.TaskEither<E, readonly T[]>
save: (entity: T) => TE.TaskEither<E, T>
delete: (id: ID) => TE.TaskEither<E, void>
}
interface DbError {
code: 'NOT_FOUND' | 'DUPLICATE' | 'CONSTRAINT' | 'CONNECTION'
message: string
cause?: unknown
}
const createUserRepository = (db: Database): Repository<DbError, User, string> => ({
findById: (id) =>
pipe(
TE.tryCatch(
() => db.query('SELECT * FROM users WHERE id = ?', [id]),
(e): DbError => ({ code: 'CONNECTION', message: String(e), cause: e })
),
TE.chain((rows) =>
rows.length === 0
? TE.left({ code: 'NOT_FOUND', message: `User ${id} not found` })
: TE.right(rows[0] as User)
)
),
findAll: () =>
TE.tryCatch(
() => db.query('SELECT * FROM users'),
(e): DbError => ({ code: 'CONNECTION', message: String(e), cause: e })
),
save: (user) =>
TE.tryCatch(
() => db.query(
'INSERT INTO users (id, name, email) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET name = ?, email = ?',
[user.id, user.name, user.email, user.name, user.email]
).then(() => user),
(e): DbError => {
if (String(e).includes('UNIQUE constraint')) {
return { code: 'DUPLICATE', message: 'Email already exists', cause: e }
}
return { code: 'CONNECTION', message: String(e), cause: e }
}
),
delete: (id) =>
TE.tryCatch(
() => db.query('DELETE FROM users WHERE id = ?', [id]).then(() => undefined),
(e): DbError => ({ code: 'CONNECTION', message: String(e), cause: e })
)
})typescript
interface Repository<E, T, ID> {
findById: (id: ID) => TE.TaskEither<E, T>
findAll: () => TE.TaskEither<E, readonly T[]>
save: (entity: T) => TE.TaskEither<E, T>
delete: (id: ID) => TE.TaskEither<E, void>
}
interface DbError {
code: 'NOT_FOUND' | 'DUPLICATE' | 'CONSTRAINT' | 'CONNECTION'
message: string
cause?: unknown
}
const createUserRepository = (db: Database): Repository<DbError, User, string> => ({
findById: (id) =>
pipe(
TE.tryCatch(
() => db.query('SELECT * FROM users WHERE id = ?', [id]),
(e): DbError => ({ code: 'CONNECTION', message: String(e), cause: e })
),
TE.chain((rows) =>
rows.length === 0
? TE.left({ code: 'NOT_FOUND', message: `User ${id} not found` })
: TE.right(rows[0] as User)
)
),
findAll: () =>
TE.tryCatch(
() => db.query('SELECT * FROM users'),
(e): DbError => ({ code: 'CONNECTION', message: String(e), cause: e })
),
save: (user) =>
TE.tryCatch(
() => db.query(
'INSERT INTO users (id, name, email) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET name = ?, email = ?',
[user.id, user.name, user.email, user.name, user.email]
).then(() => user),
(e): DbError => {
if (String(e).includes('UNIQUE constraint')) {
return { code: 'DUPLICATE', message: 'Email already exists', cause: e }
}
return { code: 'CONNECTION', message: String(e), cause: e }
}
),
delete: (id) =>
TE.tryCatch(
() => db.query('DELETE FROM users WHERE id = ?', [id]).then(() => undefined),
(e): DbError => ({ code: 'CONNECTION', message: String(e), cause: e })
)
})Transaction handling
事务处理
typescript
interface Transaction {
query: (sql: string, params?: unknown[]) => Promise<unknown>
commit: () => Promise<void>
rollback: () => Promise<void>
}
const withTransaction = <E, A>(
db: Database,
operation: (tx: Transaction) => TE.TaskEither<E, A>
): TE.TaskEither<E | DbError, A> =>
pipe(
TE.tryCatch(
() => db.beginTransaction(),
(e): DbError => ({ code: 'CONNECTION', message: 'Failed to start transaction', cause: e })
),
TE.chain((tx) =>
pipe(
operation(tx),
TE.chainFirst(() =>
TE.tryCatch(
() => tx.commit(),
(e): DbError => ({ code: 'CONNECTION', message: 'Commit failed', cause: e })
)
),
TE.orElse((error) =>
pipe(
TE.tryCatch(() => tx.rollback(), () => error),
TE.chain(() => TE.left(error))
)
)
)
)
)
// Usage
const transferFunds = (fromId: string, toId: string, amount: number) =>
withTransaction(db, (tx) =>
pipe(
TE.Do,
TE.bind('from', () => getAccount(tx, fromId)),
TE.bind('to', () => getAccount(tx, toId)),
TE.filterOrElse(
({ from }) => from.balance >= amount,
() => ({ code: 'INSUFFICIENT_FUNDS', message: 'Not enough balance' })
),
TE.chain(({ from, to }) =>
sequenceT(TE.ApplySeq)(
updateBalance(tx, fromId, from.balance - amount),
updateBalance(tx, toId, to.balance + amount)
)
),
TE.map(() => ({ success: true, amount }))
)
)typescript
interface Transaction {
query: (sql: string, params?: unknown[]) => Promise<unknown>
commit: () => Promise<void>
rollback: () => Promise<void>
}
const withTransaction = <E, A>(
db: Database,
operation: (tx: Transaction) => TE.TaskEither<E, A>
): TE.TaskEither<E | DbError, A> =>
pipe(
TE.tryCatch(
() => db.beginTransaction(),
(e): DbError => ({ code: 'CONNECTION', message: 'Failed to start transaction', cause: e })
),
TE.chain((tx) =>
pipe(
operation(tx),
TE.chainFirst(() =>
TE.tryCatch(
() => tx.commit(),
(e): DbError => ({ code: 'CONNECTION', message: 'Commit failed', cause: e })
)
),
TE.orElse((error) =>
pipe(
TE.tryCatch(() => tx.rollback(), () => error),
TE.chain(() => TE.left(error))
)
)
)
)
)
// 使用示例
const transferFunds = (fromId: string, toId: string, amount: number) =>
withTransaction(db, (tx) =>
pipe(
TE.Do,
TE.bind('from', () => getAccount(tx, fromId)),
TE.bind('to', () => getAccount(tx, toId)),
TE.filterOrElse(
({ from }) => from.balance >= amount,
() => ({ code: 'INSUFFICIENT_FUNDS', message: 'Not enough balance' })
),
TE.chain(({ from, to }) =>
sequenceT(TE.ApplySeq)(
updateBalance(tx, fromId, from.balance - amount),
updateBalance(tx, toId, to.balance + amount)
)
),
TE.map(() => ({ success: true, amount }))
)
)10. Task vs TaskEither: When to Use Which
10. Task vs TaskEither: 何时使用哪个
Use Task when:
当以下情况使用Task:
typescript
import * as T from 'fp-ts/Task'
// 1. Operation cannot fail
const delay = (ms: number): T.Task<void> =>
() => new Promise(resolve => setTimeout(resolve, ms))
// 2. Errors are handled elsewhere
const logMessage = (msg: string): T.Task<void> =>
() => console.log(msg) as unknown as Promise<void>
// 3. You want to ignore errors
const fetchOrDefault = (url: string, defaultValue: Data): T.Task<Data> =>
pipe(
TE.tryCatch(() => fetch(url).then(r => r.json()), E.toError),
TE.getOrElse(() => T.of(defaultValue))
)
// 4. Fire and forget
const trackAnalytics = (event: Event): T.Task<void> =>
() => analytics.track(event).catch(() => {}) // Errors swallowedtypescript
import * as T from 'fp-ts/Task'
// 1. 操作不会失败
const delay = (ms: number): T.Task<void> =>
() => new Promise(resolve => setTimeout(resolve, ms))
// 2. 错误在其他地方处理
const logMessage = (msg: string): T.Task<void> =>
() => console.log(msg) as unknown as Promise<void>
// 3. 你想忽略错误
const fetchOrDefault = (url: string, defaultValue: Data): T.Task<Data> =>
pipe(
TE.tryCatch(() => fetch(url).then(r => r.json()), E.toError),
TE.getOrElse(() => T.of(defaultValue))
)
// 4. 无需等待结果(即发即弃)
const trackAnalytics = (event: Event): T.Task<void> =>
() => analytics.track(event).catch(() => {}) // 忽略错误Use TaskEither when:
当以下情况使用TaskEither:
typescript
// 1. Operation can fail and you need to handle the error
const fetchUser = (id: string): TE.TaskEither<Error, User> =>
TE.tryCatch(() => api.getUser(id), E.toError)
// 2. You need typed errors for different failure modes
type AuthError =
| { type: 'INVALID_CREDENTIALS' }
| { type: 'EXPIRED_TOKEN' }
| { type: 'NETWORK_ERROR'; cause: Error }
const authenticate = (token: string): TE.TaskEither<AuthError, User> => { /* ... */ }
// 3. Error recovery is part of business logic
const getConfig = (): TE.TaskEither<ConfigError, Config> =>
pipe(
fetchRemoteConfig(),
TE.orElse(() => loadLocalConfig()),
TE.orElse(() => TE.right(defaultConfig))
)
// 4. Composing multiple fallible operations
const processOrder = (orderId: string): TE.TaskEither<OrderError, Receipt> =>
pipe(
validateOrder(orderId),
TE.chain(chargePayment),
TE.chain(fulfillOrder),
TE.chain(sendConfirmation)
)typescript
// 1. 操作可能失败且你需要处理错误
const fetchUser = (id: string): TE.TaskEither<Error, User> =>
TE.tryCatch(() => api.getUser(id), E.toError)
// 2. 你需要为不同的失败模式使用类型化错误
type AuthError =
| { type: 'INVALID_CREDENTIALS' }
| { type: 'EXPIRED_TOKEN' }
| { type: 'NETWORK_ERROR'; cause: Error }
const authenticate = (token: string): TE.TaskEither<AuthError, User> => { /* ... */ }
// 3. 错误恢复是业务逻辑的一部分
const getConfig = (): TE.TaskEither<ConfigError, Config> =>
pipe(
fetchRemoteConfig(),
TE.orElse(() => loadLocalConfig()),
TE.orElse(() => TE.right(defaultConfig))
)
// 4. 组合多个可能失败的操作
const processOrder = (orderId: string): TE.TaskEither<OrderError, Receipt> =>
pipe(
validateOrder(orderId),
TE.chain(chargePayment),
TE.chain(fulfillOrder),
TE.chain(sendConfirmation)
)Converting between them
两者之间的转换
typescript
// Task to TaskEither (infallible to fallible)
const taskToTE = <A>(task: T.Task<A>): TE.TaskEither<never, A> =>
pipe(task, T.map(E.right))
// TaskEither to Task (handle/ignore error)
const teToTask = <E, A>(te: TE.TaskEither<E, A>, defaultValue: A): T.Task<A> =>
TE.getOrElse(() => T.of(defaultValue))(te)
// TaskEither to Task (throw on error - escape hatch)
const teToTaskThrow = <E, A>(te: TE.TaskEither<E, A>): T.Task<A> =>
pipe(
te,
TE.getOrElse((e) => () => Promise.reject(e))
)typescript
// Task转TaskEither(从不会失败到可能失败)
const taskToTE = <A>(task: T.Task<A>): TE.TaskEither<never, A> =>
pipe(task, T.map(E.right))
// TaskEither转Task(处理或忽略错误)
const teToTask = <E, A>(te: TE.TaskEither<E, A>, defaultValue: A): T.Task<A> =>
TE.getOrElse(() => T.of(defaultValue))(te)
// TaskEither转Task(错误时抛出 - 逃生舱)
const teToTaskThrow = <E, A>(te: TE.TaskEither<E, A>): T.Task<A> =>
pipe(
te,
TE.getOrElse((e) => () => Promise.reject(e))
)Quick Reference
快速参考
| Operation | Function | Description |
|---|---|---|
| Create success | | Wrap value in Right |
| Create failure | | Wrap error in Left |
| From Promise | | Convert Promise to TE |
| Transform value | | Apply f to success value |
| Transform error | | Apply f to error value |
| Chain operations | | Sequence dependent operations |
| Recover from error | | Try alternative on error |
| Handle both cases | | Pattern match result |
| Parallel array | | Map + sequence in parallel |
| Sequential array | | Map + sequence in order |
| Filter with error | | Validate with error |
| Get or default | | Extract value with fallback |
| 操作 | 函数 | 描述 |
|---|---|---|
| 创建成功 | | 将值包装为Right |
| 创建失败 | | 将错误包装为Left |
| 从Promise创建 | | 将Promise转换为TE |
| 转换值 | | 对成功值应用f |
| 转换错误 | | 对错误值应用f |
| 链式调用操作 | | 顺序执行依赖操作 |
| 从错误中恢复 | | 错误时尝试备选方案 |
| 处理两种情况 | | 模式匹配结果 |
| 并行处理数组 | | Map + sequence并行执行 |
| 顺序处理数组 | | Map + sequence顺序执行 |
| 带错误的过滤 | | 验证并在不满足时返回错误 |
| 获取值或默认值 | | 提取值并提供回退 |
Common Patterns Summary
常见模式总结
typescript
// 1. Fetch with error handling
const fetch = TE.tryCatch(() => api.get(url), toError)
// 2. Chain dependent calls
pipe(getA(), TE.chain(a => getB(a.id)), TE.chain(b => getC(b.id)))
// 3. Parallel independent calls
sequenceT(TE.ApplyPar)(getA(), getB(), getC())
// 4. Build context with Do
pipe(TE.Do, TE.bind('a', () => getA()), TE.bind('b', ({a}) => getB(a)))
// 5. Recover from errors
pipe(primary(), TE.orElse(() => fallback()))
// 6. Execute and handle result
pipe(operation(), TE.fold(handleError, handleSuccess))()typescript
// 1. 带错误处理的获取
const fetch = TE.tryCatch(() => api.get(url), toError)
// 2. 链式调用依赖请求
pipe(getA(), TE.chain(a => getB(a.id)), TE.chain(b => getC(b.id)))
// 3. 并行调用独立请求
sequenceT(TE.ApplyPar)(getA(), getB(), getC())
// 4. 使用Do构建上下文
pipe(TE.Do, TE.bind('a', () => getA()), TE.bind('b', ({a}) => getB(a)))
// 5. 从错误中恢复
pipe(primary(), TE.orElse(() => fallback()))
// 6. 执行并处理结果
pipe(operation(), TE.fold(handleError, handleSuccess))()