jquense / yup

Dead simple Object schema validation
MIT License
22.95k stars 935 forks source link

Cast should return schema type #2073

Closed lorenzlars closed 1 year ago

lorenzlars commented 1 year ago

I have the issue that the return type of cast is not the type of the schema. Which, in my opinion, is then not a cast and at least to me this is a bug.

To Reproduce

type Foo = {
  bar: string;
};

function doSomething(foo: Foo) {
  ...
}

const schema = yup.object<Foo>({
  bar: yup.string(),
})

doSomething(schema.cast({})) // Argument type ResolveFlags<MakeKeysOptional<_<TypeFromShape<{}, Foo>>>, "", _<DefaultFromShape<{}>>> is not assignable to parameter type Foo

Expected behavior

Cast should cast to the schema type.

schema.ts

- cast(value: any, options?: CastOptions<TContext>): this['__outputType'];
+ cast(value: any, options?: CastOptions<TContext>): this['__context'];
  cast(
    value: any,
    options: CastOptionalityOptions<TContext>,
- ): this['__outputType'] | null | undefined;
+ ): this['__context'] | null | undefined;
  cast(
    value: any,
    options: CastOptions<TContext> | CastOptionalityOptions<TContext> = {},
- ): this['__outputType'] {
+ ): this['__context'] {
    let resolvedSchema = this.resolve({
      value,
      ...options,
      // parent: options.parent,
      // context: options.context,
    });
    let allowOptionality = options.assert === 'ignore-optionality';

    let result = resolvedSchema._cast(value, options as any);

    if (options.assert !== false && !resolvedSchema.isType(result)) {
      if (allowOptionality && isAbsent(result)) {
        return result as any;
      }

      let formattedValue = printValue(value);
      let formattedResult = printValue(result);

      throw new TypeError(
        `The value of ${
          options.path || 'field'
        } could not be cast to a value ` +
          `that satisfies the schema type: "${resolvedSchema.type}". \n\n` +
          `attempted value: ${formattedValue} \n` +
          (formattedResult !== formattedValue
            ? `result of cast: ${formattedResult}`
            : ''),
      );
    }

    return result;
  }
jquense commented 1 year ago

Did you look at what schema.cast({}) produces in this case? It's an empty object, because you have not supplied a a value for bar, The return type of cast correctly describes what it returns, Foo would be untrue in this case.

I would consult the docs for what yup means by cast and validate, we can't always produce the type you want because the input value is not valid, not all casts are valid and produce the type you want. The example is more or less equivalent to the following typescript:

type Foo = {
  bar: string;
};

const foo: Foo = {} // ERROR: Property 'bar' is missing in type '{}' but required in type 'Foo'.

Your usage of the generic in the example isn't correct either: object<Foo>() is saying the the object is shaped like a Foo but then your schema doesn't implement that. The right way to do this is like:

const schema: ObjectSchema<Foo> = object({
  bar: string(),
})

which produces an error saying your schema is wrong

lorenzlars commented 1 year ago

Foremost, thank you for your awesome work. I really love to work with yup. 😍

Issue

Sorry that I was inaccurate. I would expect that a function named cast is doing a cast. If you say this is not the case the function name is misleading.

doSomething(schema.cast({ bar: '' })) // Argument type ResolveFlags<MakeKeysOptional<_<TypeFromShape<{}, Foo>>>, "", _<DefaultFromShape<{}>>> is not assignable to parameter type Foo

Question

Both should work the same, just a different syntax. Yours is setting the type of the variable, mine is setting the generic type of the object schema function, which then sets implicitly the type of the variable. What is the reason, that makes it necessary, to do it, how you did it?

const schema: ObjectSchema<Foo> = object({
  bar: string(),
})
const schema = object<Foo>({
  bar: string(),
})

Simple example

  type Base<T> = {
    value: T
  }

  type Foo = {
    bar: string
  }

  function something<T>(value: T): Base<T> {
    return { value }
  }

  const a: Base<Foo> = something({ bar: '' })
  const a: Base<Foo> = something({  }) // Initializer type Base<{}> is not assignable to variable type Base<Foo> 
  const b = something<Foo>({ bar: '' })
  const b = something<Foo>({ }) // Argument type {} is not assignable to parameter type Foo 
jquense commented 1 year ago

I would expect that a function named cast is doing a cast

It does cast. The schema is casting correctly to the type the schema defined which is { bar?: string }, which {} is assignable too. Like i said your example tho is lying about what the type is. Its the same as this example:

type Foo = {
  bar: string;
};

const foo = {} as any as Foo

foo is now cast as a Foo, but that doesn't suddenly make the foo that shape.

Both should work the same, just a different syntax.

They aren't the same though, which is why i mentioned it.

What is the reason, that makes it necessary, to do it, how you did it?

The generic on object is not the type the schema outputs, TContext, is the the type of the context object you can pass to tests in options it has nothing to do with the schema type.