fabian-hiller / valibot

The modular and type safe schema library for validating structural data 🤖
https://valibot.dev
MIT License
6.22k stars 201 forks source link

How to create dynamic variants with the nested schemas? #757

Closed nd0ut closed 1 month ago

nd0ut commented 3 months ago

Hello!

Here is the playground MRE: https://valibot.dev/playground/?code=JYWwDg9gTgLgBAKjgQwM5wG5wGZQiOAcg2QBtgAjCGQgbgCh6BjCAO1XgCUBTVAV1IwAykwAW3EMjgBeTADoIFAFbcmMABQBvenDgBrbgE8AXPI5RgrAObqAlABp6AX1sNmbDnCExkMYEwA1ZAtkVmExCSlZDDkSELD1Qn4mJl5UQns4AG0deUUVNS1c3WTU1FRTGPIYbigydWwyVG4HYrhavChKuUsOUNT1AFEoTtbdF0ddGPzVDW1dEr4UtO7q2vqYKD4WyYWoXgEYUx5+QRFxSV2J+gBdV0YWdngAEUNWZBB-IPjwi6i4AA8ABV2gAPGqsAAm6BiAHllLNzpEAaFDJlUQA+DHqXL7U6-SKmIGOWwyDG5GJxYChDRJJZldKZHILPIIwrzFlwUoreRrOqkBpNHZtXQdaDdXo+VgDYajXbjMYLaZsuYirn0nlVYA1fnqTbbRUsvGHUzGs4RS5ta66O4MIA

Inline code:

import * as v from 'valibot';

const ResultSchema = v.object({
  key: v.string(),
});

const StaticVariantSchema = v.variant('success', [
  v.object({
    success: v.literal(false),
    error: v.instance(Error),
  }),
  v.object({
    success: v.literal(true),
    result: ResultSchema,
  }),
]);

const DynamicVariantSchema = <T extends v.ObjectSchema<any, any>>(
  resultSchema: T,
) =>
  v.variant('success', [
    v.object({
      success: v.literal(false),
      error: v.instance(Error),
    }),
    // here i'm getting error
    v.object({
      success: v.literal(true),
      result: resultSchema,
    }),
  ]);

const staticResult = v.parse(StaticVariantSchema, {
  success: true,
  result: {
    key: 'string',
  },
});

const dynamicResult = v.parse(DynamicVariantSchema(ResultSchema), {
  success: true,
  result: {
    key: 'string',
  },
});

console.log(staticResult, dynamicResult);

At runtime, it's working OK, but I got an error from TS:

Type 'ObjectSchema<{ readonly success: LiteralSchema<true, undefined>; readonly result: S; }, undefined>' is not assignable to type 'VariantOption<"success">'.
  Type 'ObjectSchema<{ readonly success: LiteralSchema<true, undefined>; readonly result: S; }, undefined>' is not assignable to type 'ObjectSchema<Record<"success", BaseSchema<unknown, unknown, BaseIssue<unknown>>>, ErrorMessage<ObjectIssue> | undefined>'.
    The types returned by '_run(...)' are incompatible between these types.
      Type 'Dataset<{ [TKey in keyof WithReadonly<{ readonly success: LiteralSchema<true, undefined>; readonly result: S; }, WithQuestionMarks<{ readonly success: LiteralSchema<true, undefined>; readonly result: S; }, InferEntriesOutput<...>>>]: WithReadonly<...>[TKey]; }, ObjectIssue | InferObjectIssue<...>>' is not assignable to type 'Dataset<{ success: unknown; }, BaseIssue<unknown> | ObjectIssue>'.
        Type 'TypedDataset<{ [TKey in keyof WithReadonly<{ readonly success: LiteralSchema<true, undefined>; readonly result: S; }, WithQuestionMarks<{ readonly success: LiteralSchema<true, undefined>; readonly result: S; }, InferEntriesOutput<...>>>]: WithReadonly<...>[TKey]; }, ObjectIssue | InferObjectIssue<...>>' is not assignable to type 'Dataset<{ success: unknown; }, BaseIssue<unknown> | ObjectIssue>'.
          Type 'TypedDataset<{ [TKey in keyof WithReadonly<{ readonly success: LiteralSchema<true, undefined>; readonly result: S; }, WithQuestionMarks<{ readonly success: LiteralSchema<true, undefined>; readonly result: S; }, InferEntriesOutput<...>>>]: WithReadonly<...>[TKey]; }, ObjectIssue | InferObjectIssue<...>>' is not assignable to type 'TypedDataset<{ success: unknown; }, BaseIssue<unknown> | ObjectIssue>'.
            Property 'success' is missing in type '{ [TKey in keyof WithReadonly<{ readonly success: LiteralSchema<true, undefined>; readonly result: S; }, WithQuestionMarks<{ readonly success: LiteralSchema<true, undefined>; readonly result: S; }, InferEntriesOutput<...>>>]: WithReadonly<...>[TKey]; }' but required in type '{ success: unknown; }'.ts(2322)

Am I doing something wrong with the generics, or is it a bug?

nd0ut commented 3 months ago

Oh, got it.

const DynamicVariantSchema = <TEntries extends ObjectEntries, TMessage extends ErrorMessage<ObjectIssue> | undefined>(
  resultSchema: ObjectSchema<TEntries, TMessage>,
) =>
  v.variant('success', [
    v.object({
      success: v.literal(false),
      error: v.instance(Error),
    }),
    v.object({
      success: v.literal(true),
      result: resultSchema,
    }),
  ]);
nd0ut commented 1 month ago

Hey @fabian-hiller, I have another, a bit more complex example:

const extendBaseSchema = <
  TEntries extends ObjectEntries,
  TMessage extends ErrorMessage<ObjectIssue> | undefined
>(
  schema: ObjectSchema<TEntries, TMessage>
) => {
  return variant("error", [
    object({
      error: literal("not found"),
      obj_type: literal("error"),
    }),
    object({
      error: literal("not authenticated"),
      obj_type: literal("error"),
      login_link: string(),
    }),
    object({
      error: undefined_(),
      ...schema.entries
    }),
  ]);
};

I'm unable to extend one of the variants with the provided object schema entries. Could suggest a solution please?

MRE: https://valibot.dev/playground/?code=JYWwDg9gTgLgBAbwFBzgUSlaBZApgZ3wEMBzXAGhTgBtgZcojrLUIAjAK1wGMYW4A8px4w0AOxhRgBfkK68AkoQCuFKnJEBlbgAtcIIv2IAzXAAUiUfLgCC+AJ5juRycDEl+ysQBNcxt7jeAPr8AG6WwEQSlAC+cMZYIHAA5OG0bBAwyQDcSEjcEGL48LgAHvQ+AEJE1tp6BnAAvHAAPFQAKuKuBHBlFd74gsK8XVIyHXiEpLi95bg+gxhYUJPEZC0aiiq4AHxwAD5wXr7+YoGUOwAUVPi6+kQAXEPyMHX3LZ0SY-jkcO2r0x2lAAlE09shUFBcDBlFAxHBwlIojBLskGMtkr8ANpUVjDFEQ1BE9HQJ60eiMaiosSZeIQY7JYH8IlwdgcIIwexgXBkugMJioklQRnMuAxJm41n4y6EllC3kUgXJGnwIjKGB6CTAbhEejeEWSvHsznchX8qlozDQA0s1DUCAkNxBWhiADWT2KUnclwlLPForZIhlht6VqgT2OfgCwR9otQADpE7d6kR4-Nuj9Jf6qABdYG5GK5IA

fabian-hiller commented 1 month ago

This works if you don't care about the exact output type.