io-ts
io-ts copied to clipboard
`chain` implementation for Decoder for interdependent decoders
🚀 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 |
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)
)