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

Best practice for valibot to work with typescript's isolated declarations feature #968

Open
hyf0 opened this issue Dec 9, 2024 · 12 comments
Assignees
Labels
question Further information is requested

Comments

@hyf0
Copy link

hyf0 commented Dec 9, 2024

With enabling https://www.typescriptlang.org/tsconfig/#isolatedDeclarations, tsc require to write code with explicit type annotations:

For exmaple:

The case in README.md

const LoginSchema: /* tsc force you write type here, so what should be write here for valibot? */ = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});

For small schemas, I could just type them there. But I have very big schemas which isn't proper to manually write them.

zod has a helper type ZodType to infer schemas from types. So for zod, it could be written as

const LoginSchema: ZodType<{ email: string, passworld: string }> = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});
@fabian-hiller
Copy link
Owner

Valibot offers GenericSchema (similar to ZodType). But in general this is not a good idea because a lot of information will be lost. I recommend to disable this rule for your schema files or find another workaround. Please keep me posted and feel free to contact me if you have any further questions.

@fabian-hiller fabian-hiller self-assigned this Dec 11, 2024
@fabian-hiller fabian-hiller added the question Further information is requested label Dec 11, 2024
@shulaoda
Copy link

shulaoda commented Dec 11, 2024

But in general this is not a good idea because a lot of information will be lost.

For simple schemas, I can directly use GenericSchema, but for more complex schemas like UnionSchema, it will be insufficient, leading to potential type conflicts.

@shulaoda
Copy link

but for more complex schemas like UnionSchema

I have conducted a thorough study and found that GenericSchema can be used in all cases, although there are some unexpected outcomes.

@shulaoda
Copy link

shulaoda commented Dec 11, 2024

This causes an type error because v.function() generates an additional type of (...args: unknown[]) => unknown. Could you tell me how to handle this? @fabian-hiller

const argsFnSchema: v.GenericSchema<(args: number[]) => boolean> = v.pipe(
  v.function(),
  v.args(v.tuple([v.array(v.number())])),
  v.returns(v.boolean()),
)

@fabian-hiller
Copy link
Owner

Yes, GenericSchema can be used in all cases. The only drawback is that some type information is lost by explicitly assigning GenericSchema instead of the actual schema interface. However, this is only a problem if you plan to use a schema as part of another schema that requires a specific schema interface.

args and returns are transformation actions because they transform the input by ensuring the correct type of function arguments and return type. Whenever you apply a transformation, you must pass the explicit output type to GenericSchema as the second generic. Here is an example:

import * as v from 'valibot';

const argsFnSchema: v.GenericSchema<
  (...args: unknown[]) => unknown,
  (yourArg: number[]) => boolean
> = v.pipe(
  v.function(),
  v.args(v.tuple([v.array(v.number())])),
  v.returns(v.boolean())
);

@shulaoda
Copy link

const argsFnSchema: v.GenericSchema<
  (...args: unknown[]) => unknown,
  (yourArg: number[]) => boolean
> = v.pipe(
  v.function(),
  v.args(v.tuple([v.array(v.number())])),
  v.returns(v.boolean())
);

Thanks for your answer, but when the schema is complex, it will become more complicated:

type ComplexSchemaType = {
  fn: (arg: string) => boolean
  argsWithFn: (arg: (arg: boolean) => string) => void
}

const ComplexSchema: v.GenericSchema<?, ComplexSchemaType> = v.strictObject({
  fn: v.pipe(
    v.function(),
    v.args(v.tuple([v.string()])),
    v.returns(v.boolean()),
  ),
  argsWithFn: v.pipe(
    v.function(),
    v.args(
      v.tuple([
        v.pipe(
          v.function(),
          v.args(v.tuple([v.boolean()])),
          v.returns(v.string()),
        ),
      ]),
    ),
  ),
})

@fabian-hiller
Copy link
Owner

You can always infer the correct input and output type by using InferInput and InferOutput and then copy them into GenericSchema. If you have other ideas on how to improve the DX in this case, I am happy to discuss them.

import * as v from 'valibot';

const ComplexSchema = v.strictObject({
  fn: v.pipe(
    v.function(),
    v.args(v.tuple([v.string()])),
    v.returns(v.boolean())
  ),
  argsWithFn: v.pipe(
    v.function(),
    v.args(
      v.tuple([
        v.pipe(
          v.function(),
          v.args(v.tuple([v.boolean()])),
          v.returns(v.string())
        ),
      ])
    )
  ),
});

type Input = v.InferInput<typeof ComplexSchema>;
type Output = v.InferOutput<typeof ComplexSchema>;

@shulaoda
Copy link

then copy them into GenericSchema

Yeah. But, this doesn't seem to help with explicitly declaring the type, because ts error ComplexSchema is directly or indirectly referenced in its own type annotation.

@fabian-hiller
Copy link
Owner

The idea is to hover over Input and Output and copy the inferred types into GenericSchema.

@fabian-hiller
Copy link
Owner

I still think it would be best to disable the isolated declaration rules for such cases, but not sure if this is possible.

@shulaoda
Copy link

shulaoda commented Dec 12, 2024

Actually, I found that type Output = v.InferOutput<typeof ComplexSchema> will be deduced as:

type Output = {
    fn: (args_0: string) => boolean;
    argsWithFn: (args_0: (...args: unknown[]) => unknown) => unknown;
}

Is this the expected behavior? Is there a problem with my writing for the args_0 of the argsWithFn?

@shulaoda
Copy link

I still think it would be best to disable the isolated declaration rules for such cases, but not sure if this is possible.

Thank you very much.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants