gcanti / io-ts

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

Getting an error report after `is` fails #560

Open gavacho opened 3 years ago

gavacho commented 3 years ago

🚀 Feature request

Current Behavior

I use is to narrow my type like:

assert(exampleCodec.is(response.body));
return response.body.some.typed.property;

The problem I have is that when there is a failure, I don't receive a detailed error like decode provides.

I considered creating this function:

function assertIs<T>(codec: Type<T>, value: unknown): asserts value is T {
  if (!codec.is(value)) {
    const message = PathReporter.report(codec.decode(value));
    throw new Error(message);
  }
}

But because some codecs are decoders, decode isn't guaranteed to fail every time is fails:

NumberFromString.is('4') // false
NumberFromString.decode('4') // right

Desired Behavior

Would it be possible to add assert to the Type interface that would behave as a TypeScript Assert Function and also provide a detailed error messages the way decode does?

Then I would be able to do:

exampleCodec.assert(response.body);
return response.body.some.typed.property;
mlegenhausen commented 3 years ago

Just for clarification in your assertIs function you use Type<T> which is equivalent to Type<T, T> but NumberFromString is Type<number, string>. So you shouldn't be able to use it in your assertIs function?

gavacho commented 3 years ago

Ah, so that's another reason why assertIs isn't a viable solution. I think to accomplish what I want (getting a failure report for when is fails) will require a change to io-ts?

mlegenhausen commented 3 years ago

Why not simply use decode as your assert function. I normally have a cast function defined for unsafe casting where I know it is ok to throw.

import * as E from 'fp-ts/Either'
import { flow } from 'fp-ts/function'
import * as t from 'io-ts'
import { failure } from 'io-ts/PathReporter'

export function cast<O, A>(codec: t.Type<A, O>): (value: O) => A
export function cast<I, A>(codec: t.Decoder<I, A>): (value: I) => A
export function cast<I, A>(codec: t.Decoder<I, A>): (value: I) => A {
  return flow(
    codec.decode,
    E.getOrElse<t.Errors, A>(errors => {
      throw new Error(failure(errors).join('\n'))
    })
  )
}
gavacho commented 3 years ago

Why not simply use decode as your assert function.

This is possible and it is what I'm doing today. The reason why I have opened this ticket is because that decode-function-which-throws is not (and can not be) a TypeScript Assert Function.

TypeScript Assert Functions allow us to do something that your cast doesn't. Specifically, this is possible with a TypeScript Assert Function:

const response = await fetch(...);
assert(response.ok);
MyCodec.assert(response.body);
return response.body.something.typed;

If I wanted to accomplish the same thing using your cast, I would be required to introduce a new variable like this:

const repsonse = await fetch(...);
assert(response.ok);
const body = cast(MyCodec)(response.body);
return body.something.typed;

In a test scenario which is hitting many endpoint, these extra variables add up! I can avoid them now by doing:

const response = await fetch(...);
assert(response.ok);
assert(MyCodec.is(response.body));
return response.body.something.typed;

But as I mentioned before, the downside with this approach is that I don't get good error messages when is fails.

Since codecs provide a TypeScript User-Defined Type Guard (e.g. MyCodec.is) it seems reasonable to want to have a TypeScript Assertion Function as well.

mlegenhausen commented 3 years ago

When it comes to assert functions I am not the right person to argue about with them cause I never used them or ever had the need to use them.

But what you describe is normally a classic use case for io-ts using the decode function. In this case is more powerful or how do you handle something like the transformation of DateISOString to Date Objects?

gavacho commented 3 years ago

When it comes to assert functions I am not the right person to argue about with them cause I never used them or ever had the need to use them.

It sounds like the way you use io-ts would not change if the is method were removed from the Type interface. Because my request provides a very subtle (but valuable) benefit, it might be hard to identify.

But what you describe is normally a classic use case for io-ts using the decode function.

Yes. I use a function very similar to your cast in my code. I understand that decode can be used to accomplish something similar to what I'm requesting. I am requesting something that can not be accomplished with decode.

In this case is more powerful or how do you handle something like the transformation of DateISOString to Date Objects?

Yes, this case is more powerful.

Let's imagine a codec like:

const Point = t.strict({
  x: NumberFromString,
  y: NumberFromString,
});

And let's imagine two different data structures:

const numberPoint = { x: 100, y: 100 };
const stringPoint = { x: '200', y: '200' };

If we used decode, it would pass stringPoint and fail numberPoint, right? But if we used is then it would pass numberPoint and fail stringPoint. This demonstrates that decode does not do the same thing as is.

If you agree that is does not do the same thing as decode, then can you agree that it would be useful for users of is to have detailed error messages (like the ones available when calling decode)?

SHaTRO commented 3 years ago

I don't think the additional error reporting from is should be necessary because it should be used as a discriminator. Either we've obtained our decoded value by actually decoding or we are using a type system that should be relieving us of the need for detailed error on discrimination.

I don't think that arbitrary values requiring knowledge as to "why" something isn't something else should really exist if we are coding correctly. Certainly there may be use cases that I'm unaware of. But I don't think adding this functionality to is makes it "more powerful" - it just makes it more cumbersome. Plus - more importantly - where would these alleged "errors" be reported?

Perhaps this is just a misunderstanding or misuse of is leading to the idea that additional information is required. Use it as a discriminator for sum types. That is the purpose.

If validation of the "assert" notion is needed, it should be done at the point of decoding.