fp-ts-task-either

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

fp-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>>
  • E
    = Error type (left)
  • A
    = Success type (right)
  • 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 swallowed
typescript
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

快速参考

OperationFunctionDescription
Create success
TE.right(value)
Wrap value in Right
Create failure
TE.left(error)
Wrap error in Left
From Promise
TE.tryCatch(promise, onError)
Convert Promise to TE
Transform value
TE.map(f)
Apply f to success value
Transform error
TE.mapLeft(f)
Apply f to error value
Chain operations
TE.chain(f)
/
TE.flatMap(f)
Sequence dependent operations
Recover from error
TE.orElse(f)
Try alternative on error
Handle both cases
TE.fold(onError, onSuccess)
Pattern match result
Parallel array
TE.traverseArray(f)
Map + sequence in parallel
Sequential array
A.traverse(TE.ApplicativeSeq)(f)
Map + sequence in order
Filter with error
TE.filterOrElse(pred, onFalse)
Validate with error
Get or default
TE.getOrElse(onError)
Extract value with fallback

操作函数描述
创建成功
TE.right(value)
将值包装为Right
创建失败
TE.left(error)
将错误包装为Left
从Promise创建
TE.tryCatch(promise, onError)
将Promise转换为TE
转换值
TE.map(f)
对成功值应用f
转换错误
TE.mapLeft(f)
对错误值应用f
链式调用操作
TE.chain(f)
/
TE.flatMap(f)
顺序执行依赖操作
从错误中恢复
TE.orElse(f)
错误时尝试备选方案
处理两种情况
TE.fold(onError, onSuccess)
模式匹配结果
并行处理数组
TE.traverseArray(f)
Map + sequence并行执行
顺序处理数组
A.traverse(TE.ApplicativeSeq)(f)
Map + sequence顺序执行
带错误的过滤
TE.filterOrElse(pred, onFalse)
验证并在不满足时返回错误
获取值或默认值
TE.getOrElse(onError)
提取值并提供回退

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))()