Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple errors/custom error types from a decoded type. #478

Closed
chrischen opened this issue Jun 10, 2020 · 11 comments
Closed

Multiple errors/custom error types from a decoded type. #478

chrischen opened this issue Jun 10, 2020 · 11 comments
Labels
experimental something related to the experimental features needs more info

Comments

@chrischen
Copy link

chrischen commented Jun 10, 2020

🚀 Feature request

I'm wondering what's the best way to do a refinement/decode that may have multiple validations/errors that need to be returned.

In particular I'd like to implement the error handling method as espoused in Railway Oriented Programming: https://fsharpforfunandprofit.com/rop/. Errors are treated as Domain Events and you check for events in a tagged union of the accumulated errors at the end of a workflow/pipeline.

When checking a particular string, such as email, I may want to check for multiple properties and return them as error codes (as opposed to strings). These error codes could wrap data, for example:
{_tag: 'InvalidEmail', value: 'someinvalidemail@adfsdfa'}

Current Behavior

// This works
const decoder: Decode<string> = { decode: (x) => D.failure('Can only put a string here') }
// Type error
const decoder: Decode<string> = { decode: (x) => D.failure(['String must end in period.', 'string must be in all lowercase']) }
// Type error
const decoder: Decode<string> = { decode: (x) => D.failure([{ _tag: 'NoPeriod' }, { _tag: 'MustBeLowercase'}]) }

Desired Behavior

const decoder: Decode<string> = { decode: (x) => D.failure([{ _tag: 'NoPeriod' }, { _tag: 'MustBeLowercase'}]) }

Suggested Solution

I would imagine the DecodeError type should be generic to allow the user to specify complex shapes to hold the errors.

Who does this impact? Who is this for?

https://fsharpforfunandprofit.com/rop/ -> Trying to follow patterns espoused here-particularly the stuff around error handling as "messages".

Describe alternatives you've considered

The alternative is to define a new type for each validation case. Eg:

const Email = D.string
const EmailWithAtSign = { decode: (x): x is EmailWithAtSign => ... }
const EmailWithPeriod = { decode: (x): x is EmailWithDomain => ... }

Additional context

Your environment

Software Version(s)
io-ts 2.2
fp-ts n/a
TypeScript n/a
@chrischen chrischen changed the title Multiple errors from a decoded type. Multiple errors/custom error types from a decoded type. Jun 10, 2020
@gcanti gcanti added needs more info experimental something related to the experimental features labels Jun 12, 2020
@gcanti
Copy link
Owner

gcanti commented Jun 12, 2020

I would imagine the DecodeError type should be generic to allow the user to specify complex shapes to hold the errors.

@chrischen Please expand on this, how you'd define Decoder? How you will combine this new kind of decoders?

@chrischen
Copy link
Author

chrischen commented Jun 12, 2020

For custom errors I think maybe provide a semigroup or monoid?

This is the shape of the error type I've implemented, where A is the decoded type.

Either<DomainErrors, [A, DomainMessages]>

But actually on second thought I think custom errors may not be necessary. If the default error system can just accumulate an arbitrary list of user defined errors that should be fine.

It's hard to use the current error reporting unless I just use them as is. If I wrap a decoder then I lose out on granular type errors. I'd like to be able to rely on the error types to do my own logic such as converting the codes to messages. It's also good to have them in one place in case localization is needed.

So I'd like to be able to not be limited to string errors. I'm using this tagged type as an example as the value field can be used to pass the context of what the bad input was. Current concatenation of errors should still work right?

const decoder: Decode<string> = { decode: (x) => D.failure({ type: 'EmailInvalidError': value: 'invalid@email' }) }

@chrischen
Copy link
Author

chrischen commented Jun 13, 2020

So I looked through the source and found withExpected which can let me override the default error reporting, but it's still limited to strings. I think instead of Tree for DecodeError, it should be Tree generic with some error object that can be user specified.

Here is what I'm trying to get my error handling to look like:
https://fsprojects.github.io/Chessie/railway.html
https://fsprojects.github.io/Chessie/reference/chessie-errorhandling-result-2.html

Also on a slightly unrelated note, intersection(a, b) does not report errors for b if a has a decode failure, whereas union and type are reporting all the errors.

@gcanti
Copy link
Owner

gcanti commented Jun 16, 2020

Also on a slightly unrelated note, intersection(a, b) does not report errors for b if a has a decode failure, whereas union and type are reporting all the errors.

Thanks for reporting, fixed in v2.2.6

@gcanti
Copy link
Owner

gcanti commented Jun 20, 2020

@chrischen this is the more general form of a "decoder" I can think of

export interface DecoderT<M extends URIS2, E, A> {
  readonly decode: (u: unknown) => Kind2<M, E, A>
}

where the error E and (even) the effect M are not yet specified (DecoderT is kind of a "monad transformer").

Then you can define some constructors

export const fromGuard = <M extends URIS2, E>(M: MonadThrow2C<M, E>) => <A>(
  guard: G.Guard<A>,
  onError: (u: unknown) => E
): DecoderT<M, E, A> => {
  return {
    decode: (u) => (guard.is(u) ? M.of(u) : M.throwError(onError(u)))
  }
}

and primitives

export const string = <M extends URIS2, E>(M: MonadThrow2C<M, E>) => (
  onError: (u: unknown) => E
): DecoderT<M, E, string> => {
  return fromGuard(M)(G.string, onError)
}

Here you can find a POC.

As you can see from the tests, depending on the chosen MonadThrow instance, you can define

@gcanti
Copy link
Owner

gcanti commented Jun 22, 2020

I pushed this thing further, I got a more fleshed out Kleisli module (branch 478) from which I can derive:

  • Decoder.ts: re-implementation of Decoder.ts but uses FreeSemigroup instead of NonEmptyArray as semigroup and this DecoderError module as errors
  • TaskDecoder.ts: async decoders

@chrischen for what concerns the original issue, Kleisli is fully configurable so you should be able to use your own errors

Example

import * as E from 'fp-ts/lib/Either'
import * as NEA from 'fp-ts/lib/NonEmptyArray'
import { pipe } from 'fp-ts/lib/pipeable'
import * as G from '../src/Guard'
import * as K from '../src/Kleisli'

type MyError = { type: 'string' } | { type: 'NonEmptyString' }

interface Decoder<A> {
  readonly decode: (u: unknown) => E.Either<NEA.NonEmptyArray<MyError>, A>
}

const M = E.getValidation(NEA.getSemigroup<MyError>())

const string: Decoder<string> = K.fromRefinement(M)(G.string.is, () => [{ type: 'string' }])

const NonEmptyString: Decoder<string> = pipe(
  string,
  K.refine(M)(
    (s): s is string => s.length > 0,
    () => [{ type: 'NonEmptyString' }]
  )
)

console.log(NonEmptyString.decode(null)) // { _tag: 'Left', left: [ { type: 'string' } ] }
console.log(NonEmptyString.decode('')) // { _tag: 'Left', left: [ { type: 'NonEmptyString' } ] }

@chrischen
Copy link
Author

Awesome! This matches my workaround of wrapping the decoder to return my own error. When do you expect this to be released?

@gcanti
Copy link
Owner

gcanti commented Jun 29, 2020

Closed by #486

@gcanti gcanti closed this as completed Jun 29, 2020
@yemi
Copy link

yemi commented Sep 30, 2020

Would this work together with the default Decoder or would I have to create my own decoders for all types im interested in, e.g. D.type since the error type changed? Is it easy enough to use io-tss decoders when using my own errors?

@yemi
Copy link

yemi commented Oct 22, 2020

Nvm, realized I had to create my own Decoder module, which now works great, thanks for the Kleisli module 👍

@JohnGurin
Copy link

JohnGurin commented Nov 11, 2022

example above
Cannot figure out how to replace deprecated getValidation with getAltValidation or getAplicativeValidation for custom errors. Are any updated guides on the matter?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
experimental something related to the experimental features needs more info
Projects
None yet
Development

No branches or pull requests

4 participants