io-ts icon indicating copy to clipboard operation
io-ts copied to clipboard

`chain` implementation for Decoder for interdependent decoders

Open vicrac opened this issue 4 years ago • 1 comments

🚀 Feature request

Current Behavior

Currently it's not clear to me how can I accurately express that one decoder depends on value of another.

Desired Behavior

import * as D from "io-ts/Decoder"

declare function chain<A, B>(
  fn: (a: A) => D.Decoder<unknown, B>
): (da: D.Decoder<unknown, A>) => D.Decoder<unknown, B>

Having such a function, one can easy e.g. write decoder for versioned data:

const decoder: D.Decoder<unknown, { data: Array<string> }> = pipe(
  D.struct({ version: D.number }),
  chain(({ version }) =>
    D.struct({
      data:
        version === 1
          ? pipe(
              D.string,
              D.map((s) => [s])
            )
          : D.array(D.string),
    })
  )
)

Suggested Solution

function chain<A, B>(
  fn: (a: A) => D.Decoder<unknown, B>
): (da: D.Decoder<unknown, A>) => D.Decoder<unknown, B> {
  return (da) => ({
    decode: (value: unknown) =>
      pipe(
        value,
        da.decode,
        E.fold(
          (err) => D.failure(value, D.draw(err)),
          (a) => fn(a).decode(value)
        )
      ),
  })
}

Who does this impact? Who is this for?

Mostly folks needing versioned/multiformat data decoding.

Describe alternatives you've considered

Using D.parse - less concise, since inside parse I'd possibly have to nest another decoder and fold it to D.success or D.failure.

Additional context

Your environment

Software Version(s)
io-ts 2.2.16
fp-ts 2.10.5
TypeScript 4.0.0

vicrac avatar Oct 14 '21 15:10 vicrac

Same here trying to using Decoder for validating user's file upload where I need to validate against FileList and then File.

File could be another decoder where properties like size, type, etc. are decoder's inputs.

EDIT: It seems that D.compose is the solution. For my use case, a first iteration is something like this:

import * as D from 'io-ts/Decoder'
import { pipe } from 'fp-ts/function'
import { readonlyNonEmptyArray } from 'fp-ts'

const fileDecoder = pipe(
  D.fromRefinement(
    (u): u is FileList => u instanceof FileList,
    'fileList'
  ),
  D.map(Array.from),
  D.compose(
    nonEmptyArray(
      D.fromRefinement(
        (u): u is File => u instanceof File,
        'file'
      )
    )
  ),
  D.map(readonlyNonEmptyArray.head)
)

f15u avatar Oct 27 '22 22:10 f15u