首页 > 解决方案 > 有没有一种更简洁的方法可以在 TypeScript 中表达许多嵌套的地图函数?

问题描述

给定下面的代码,有没有更简洁的方法可以在 TypeScript 中表达许多嵌套的 map 函数?我喜欢这个用例的 Scala “理解”,但我在 TypeScript 中找不到等价物。我觉得我在这里遗漏了一些非常明显的东西。

由于验证原因,我有几个对象可能无法实例化,因此返回类型都是Either<string, T>. 例如:

const userId: Either<string, UserId> = UserId.create('1234')

当组合由许多类似上述语句组成的对象时,看起来很粗糙。为了便于阅读,示例中的所有变量都已替换为字符串。

在 TypeScript 中,这就是我正在做的事情。有没有更简洁的方式来表达这一点而不会丢失我的类型?

const userSettings: Either<string, UserSettings> = UserId.create('1234').chain(userId => {
  return Email.create('hello@world.com').chain(email => {
    return Active.create(183).chain(active => {
      return Role.create('admin').map(role => {
        return UserSettings(userId, email, active, role)
      })
    })
  })
})

在 Scala 中,我会这样表达上面的代码:

for {
  userId <- UserId.create('1234')
  email  <- Email.create('hello@world.com')
  active <- Active.create(183)
  role   <- Role.create('admin')
} yield UserSettings(userId, email, active, role)

我正在将Purify库用于诸如 Either 之类的类型。

有没有人有任何提示、建议和/或库可以帮助清理我的嵌套地图函数 TypeScript 混乱?

标签: typescriptfunctional-programmingpurity

解决方案


你可以使用这样的东西:

const userSettings = Right({})
  .chain(acc => UserId.create('1234').map(userId => ({...acc, userId})))
  .chain(acc => Email.create('hello@world.com').map(email => ({...acc, email})))
  .chain(acc => Active.create(183).map(active => ({...acc, active})))
  .chain(acc => Role.create('admin').map(role => ({...acc, role})))
  .map(({userId, email, active, role}) => UserSettings(userId, email, active, role))

您还可以定义一个辅助函数:

// This implementation works for all functors, but the types only work for
// Either due to TypeScript's lack of HKTs
const bind =
  <N extends string, A extends object, L, R>(
    name: Exclude<N, keyof A>,
    f: (acc: A) => Either<L, R>
  ) =>
  (acc: A): Either<L, A & Record<N, R>> =>
    f(acc).map(r => ({...acc, [name]: r} as A & Record<N, R>))

const userSettings: Either<string, UserSettings> = Right({})
  .chain(bind('userId', () => UserId.create('1234')))
  .chain(bind('email', () => Email.create('hello@world.com')))
  .chain(bind('active', () => Active.create(183)))
  .chain(bind('role', () => Role.create('admin')))
  .map(({userId, email, active, role}) => UserSettings(userId, email, active, role))

拥有bind一个函数允许这样的事情:

Right({})
  .chain(bind('a', () => Right(1)))
  // The value of b depends on a
  .chain(bind('b', ({a}) => Right(a + 1)))
  // a is 1 and b is 2
  .map(({a, b}) => `a is ${a} and b is ${b}`)

这几乎是fp-ts 实现 do notation 的一个端口,因此所有功劳都归功于 Giulio Canti 和 fp-ts 的贡献者。

如果你写() =>了很多,你可以使用另一个助手:

// This could definitely be named better
const bind_ = <N extends string, A extends object, L, R>(
  name: Exclude<N, keyof A>,
  either: Either<L, R>
): ((acc: A) => Either<L, A & Record<N, R>>) => bind(name, () => either)

const userSettings: Either<string, UserSettings> = Right({})
  .chain(bind_('userId', UserId.create('1234')))
  .chain(bind_('email', Email.create('hello@world.com')))
  .chain(bind_('active', Active.create(183)))
  .chain(bind_('role', Role.create('admin')))
  .map(({userId, email, active, role}) => UserSettings(userId, email, active, role))

游乐场链接


推荐阅读