gcanti / io-ts

Runtime type system for IO decoding/encoding
https://gcanti.github.io/io-ts/
MIT License
6.68k stars 330 forks source link

Use io-ts without explicit io-fp #594

Open noobgramming opened 3 years ago

noobgramming commented 3 years ago

🚀 Feature request

Current Behavior

io-ts model validation requires knowing and using io-fp

Desired Behavior

io-ts provides traditional JS interface for model validation that decodes or throws exception

Suggested Solution

try{
const result = Model.decodeOrThrow({some: 'json'});
}
catch(err){
//handle
  console.log(PathReporter.report(err));
}

Who does this impact? Who is this for?

all typescript users

Describe alternatives you've considered

I have tried a bunch of things

Additional context

It's incredibly frustrating to try to use io-ts for model validation (where its admittedly the best library out there) when you don't desire io-fp in your codebase.

Background: us, like many others, already have RXJS and several other FP libraries in our codebase. Another isn't desired. Right now there's a hard dependency on io-fp for using io-ts which is a mental burden of learning another huge functional programming library just to use model validation.

I am fine with the libraries depending on each-other. I just wish there was a way to use model validation with normal JS error handling.

I have spent hours trying to write a helper that massages Either into a resultOrThrow so I can integrate io-ts model validation with the rest of our codebase

Your environment

Software Version(s)
io-ts latest
fp-ts latest
TypeScript latest

EDIT: this is what I've come up with for our usage. I have no idea what a "good solution" would look like, but maybe some kind of wrapper like I used would be fine?

export const validateModelOrThrow = <T>(obj: Either<t.Errors, T>): T =>
  pipe(
    obj,
    fold(
      err => {
        throw new Error(PathReporter.report(E.left(err)).join('\n'));
      },
      tt => tt,
    ),
  );
// usage
try{
const result = validateModelOrThrow(Model.decode({some: 'json'}));
}
catch(err){
//handle
  console.log(err);
}
DenisFrezzato commented 3 years ago

This library was created with TypeScript in mind and throwing errors is not type safe. So I think this feature would not be welcomed.

If you really want to throw an error when the decoding fails, you can fold, like in your example, or getOrElse, which is just fold with identity as right handler.

mixedCase commented 3 years ago

I have no idea what a "good solution" would look like

Don't learn "a library". Learn functional programming.

fp-ts uses the same concepts as all other functional languages and libraries, so it's not yet another library, and io-ts exposes you basically to just Either. If you want to have an unsafe decode that throws on failure, just include the wrapper for throwing on Left in your codebase, it's just 4 lines of code.

mmkal commented 3 years ago

+1 @noobgramming rather than requesting this pretty fundamental change in io-ts, you may want to look at zod, which has a similar API but has an "unsafe" parse method which throws with user-friendly errors when parsing fails, and encapsulates its own Either equivalent without using a dependency.

igrep commented 2 years ago

@noobgramming You can write it more simply like this:

import * as Either from 'fp-ts/Either';
import * as t from 'io-ts';
import { PathReporter } from 'io-ts/lib/PathReporter';

export function decodeOrThrow<A, O, I>(typ: t.Type<A, O, I>, i: I): A {
  return Either.getOrElse<t.Errors, A>(
    (err) => { throw new Error(PathReporter.report(Either.left(err)).join('\n')) }
  )(typ.decode(i));
}

And usage:

decodeOrThrow(Model, {some: 'json'})
silasdavis commented 2 years ago

It's very clearly an aim of this library to push on FP in general and fp-ts in particular.

Clearly this is part of the cake for @gcanti (https://twitter.com/GiulioCanti/status/1235521019131170817) who must have poured thousands of hours into this for free.

It's incredibly frustrating to try to use io-ts for model validation (where its admittedly the best library out there) when you don't desire io-fp in your codebase.

It's trivial to wrap up the fp-ts interfaces in a more traditional one, I would suggest writing and publishing your own veneer library if it is really that frustrating. I for one would prefer not to see the maintenance burden increased for cosmetic features like this to the detriment of the core functionality. It's not incidental that io-ts works so well and happens to be built on fp-ts.

Similar to @igrep's solution, the two functions below have completely eliminated the need to import fp-ts modules elsewhere in a large codebase using io-ts extensively:

export function parse<T, O, I>(
  schema: Type<T, O, I>,
  payload: I,
  ...validators: ((payload: T) => string | undefined)[]
): T {
  return parseThenOrElse(
    schema,
    payload,
    (t) => t,
    (errs) => {
      throw new Error(
        `Failed to parse payload: ${JSON.stringify(payload, null, 2)}\nErrors:\n${formatValidationErrors(errs)}`,
      );
    },
    ...validators,
  );
}

// Parses a value with the given schema and return the value of then(t: T) if it succeeds or orElse(errs: Errors) if it
// fails.
export function parseThenOrElse<T, O, I, V, D>(
  schema: Type<T, O, I>,
  payload: I,
  then: (t: T) => V,
  orElse: (errs: t.Errors) => D,
  ...validators: ((payload: T) => string | undefined)[]
): V | D {
  return pipe(
    payload,
    schema.decode,
    match<t.Errors, T, V | D>(orElse, (parsed: T) => {
      const validationErrors = compact(validators.map((v) => v(parsed)));
      if (validationErrors.length) {
        throw new Error(
          `Invalid payload: ${JSON.stringify(payload, null, 2)}\nErrors:\n${validationErrors.join('\n')}`,
        );
      }
      return then(parsed);
    }),
  );
albertodiazdorado commented 1 year ago

What you are asking for is the zod library. Which, among other things, explains that it exists in the same ecosystem as io-ts simply because it offers the same power without the Functional Programming "overhead".

This thread should be closed btw.