-
-
Notifications
You must be signed in to change notification settings - Fork 201
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
Add a fallback override for pipe
method to accept unlimited pipe items
#643
Comments
Thanks for creating this issue. I understand your point, but I am not sure if we should change const Schema = v.pipe(v.pipe(v.string(), ...), ...); The function signature you are looking for is probably the function signature of the current overload implementation. I am open to investigating if this works and makes sense for dynamically creating pipelines. export function pipe<
const TSchema extends BaseSchema<unknown, unknown, BaseIssue<unknown>>,
const TItems extends PipeItem<unknown, unknown, BaseIssue<unknown>>[],
>(...pipe: [TSchema, ...TItems]): SchemaWithPipe<[TSchema, ...TItems]>; |
Thanks for your explanation. For reference, my use case is similar to case 2 I mentioned above: I will build pipe items dynamically at first, and would like to assemble them to the final schema later. I'm stuck at this step because of this type limitation. const validationComponents = someBuilder()
const schema = pipe(
validationComponents.baseSchema,
...validationComponents.pipeItems, // ts error: A spread argument must either have a tuple type or be passed to a rest parameter.
) In this scenario I don't really care about the precision of type inference. Because I already lose such type information of valibot actions at the first step. So I'm just expecting a generic type here. OTOH, if the nesting pipelines are completely equivalent to a single pipe call with multiple items, it may be a valuable workaround for my case. |
Can you share the code of |
Because the data source is dynamic, it's impossible to analyze the number of built pipe items statically.
I will try nesting pipelines firstly. If works it will make things easier. Ignoring error is the last resort and nobody likes it. |
Also, feel free to investigate if this overload signature would solve your problem. If so, I will consider adding it. export function pipe<
const TSchema extends BaseSchema<unknown, unknown, BaseIssue<unknown>>,
const TItems extends PipeItem<unknown, unknown, BaseIssue<unknown>>[],
>(...pipe: [TSchema, ...TItems]): SchemaWithPipe<[TSchema, ...TItems]>; |
It should be. Actually I have tried it but its type inference result is incomprehensible for me. I'm stuck at writing a passing type test for it. |
Let me know if you think I can help you. |
I came across this nine pipe issue fairly recently, which was a little frustrating to deal with. // If the first item is always enforced (in pipe function below), the empty array checks technically aren't needed
// ...but this utility isn't aware of that constraint, so it is technically still needed here
type InferPipe<
TSchema extends BaseSchema<unknown, unknown, BaseIssue<unknown>>,
TRestPipeItems extends PipeItem<unknown, unknown, BaseIssue<unknown>>[],
TPrevPipeItem extends PipeItem<unknown, unknown, BaseIssue<unknown>> | null = null;
> = {
0: [];
1: TRestPipeItems extends [
infer CurrPipeItem extends PipeItem<unknown, unknown, BaseIssue<unknown>>,
...infer RestPipeItems extends PipeItem<unknown, unknown, BaseIssue<unknown>>[]
]
? [
PipeItem<InferOutput<TPrevPipeItem extends null ? TSchema : TPrevPipeItem>, InferOutput<CurrPipeItem>, InferIssue<CurrPipeItem>>,
...InferPipe<TSchema, RestPipeItems, CurrPipeItem>
]
: [];
}[TRestPipeItems extends [] ? 0 : 1];
// The tweaked pipe function (with only one pipe function needing to be defined):
function pipe<
const TSchema extends BaseSchema<unknown, unknown, BaseIssue<unknown>>,
// Enforce at least one item in the pipe
const TItems extends [PipeItem<unknown, unknown, BaseIssue<unknown>>, ...PipeItem<unknown, unknown, BaseIssue<unknown>>[]]
>(
schema: TSchema,
...items: TItems
): SchemaWithPipe<[TSchema, ...InferPipe<TSchema, TItems>]>;
// Example usage:
const schema = pipe(
string(),
minLength(1, 'Message'),
maxLength(2, 'Message'),
); Here are some of the TS outputs: |
Thank you for your research. Unfortunately, your implementation does not work for transformations. Also, the types are not correctly inferred. In your example, the resulting type of How did you reach the 9 item limit? Can you share that scheme with me? We could extend the limit to more items. |
I reached the nine limit too : my schema contains a lot of forward+partialCheck |
No worries, thanks for the reply. Ahh yes, I see, it was still a fun little TS learning experience nonetheless. :) So, why is there currently a 9 pipe limit, was that what you thought was a sensible limit, or is something specific causing the limitation? I am assuming that it is the first of those options considering that you said you can potentially extend it? I hit the 9 pipe limit in a very similar way to @TeChn4K. I have a Here is the minified schema: const minLengthSchema = (length: number) => v.pipe(v.string(), v.minLength(length));
const schema = v.pipe(
v.object({
hasAlternativeContact: v.boolean(),
firstName: v.pipe(
v.string(),
v.maxLength(200, validationMessages.maxChars(200))
),
middleName: v.pipe(
v.string(),
v.maxLength(200, validationMessages.maxChars(200))
),
lastName: v.pipe(
v.string(),
v.maxLength(200, validationMessages.maxChars(200))
),
contactNo: v.pipe(
v.string(),
v.maxLength(20, validationMessages.maxChars(20))
),
// ...with even more attributes with very similar schemas
}),
// Enforce that `firstName` has length if `hasAlternativeContact` is `true`
v.forward(
v.check(({ hasAlternativeContact, firstName } }) => {
return hasAlternativeContact
? v.safeParse(minLengthSchema(1), firstName).success
: true;
}, validationMessages.requiredField),
['firstName']
),
// ...with even more forward/check for all attributes in the object
) This quickly caused me to go over the |
I would add that |
Thanks to both of you for your feedback! For simple data types, 9 is probably enough, but I did not think about more complex cases when I set the limit. We can extend the limit to any number. It just adds more TypeScript code. The JavaScript output remains the same. What limit do you think is appropriate? Is 19 enough or should we increase it to 29 or more? |
About twenty should be enough for me, but maybe not in the futur, I can't really known now. Too bad it can't be dynamic :/ Is there any bottleneck to do the following? let baseSchema = v.object({
...
});
let pipedSchema1 = = v.pipe(baseSchema, v.forward(v.partialCheck(...), ['...']));
let pipedSchema2 = = v.pipe(pipedSchema1, v.forward(v.partialCheck(...), ['...']));
let pipedSchema3 = = v.pipe(pipedSchema2, v.forward(v.partialCheck(...), ['...']));
...
let pipedSchemaN = = v.pipe(pipedSchemaN-1, v.forward(v.partialCheck(...), ['...'])); |
No problem. Yeah, would be nice to be dynamic @TeChn4K, but you would most likely run into a lot of TS hurdles, and if not implemented in a different way to what I suggested above, would cause a Typescript excessively deep warning pretty quickly - would be really cool if someone worked out a way to do it though if it is even possible - someone send this to Matt Pocock :'). |
No, this should work fine. You can extend a pipe by nesting it as deep as you like.
I will think about it and probably raise the limit to 19 or 29 in the next version. As TypeScript improves its inference capabilities, it may be possible to dynamically type the pipe items in the future. |
I have increased the limit in the latest version to 19 |
@fabian-hiller |
You are right! Thanks for the tip! |
@stackoverfloweth I just saw that you downvoted my comment about increasing to 19 items. Feel free to give me feedback on that. |
It just that it circumvents the problem. I'm building composables on top of valibot that I'd like to take a pipe array arg for. I also really don't like the syntax of 19 overloads and would prefer not to have to reproduce that in my codebase to support pipe args. |
I agree. I would do it differently if it were possible with TypeScript, but I couldn't find a workaround. Can you find one? |
Not sure if I have misunderstood the issue at hand here, but I'll mention my suggestion anyways. There is an extreme problem on TypeHero that is all about working around the depth limit of TS, that from understanding, is the problem that is preventing there being an implicit definition of the pipe function without explicit overload definitions. That could potentially ignite ideas on how to apply it to Valibot's types even if it isn't the direct solution. Here is the problem: https://typehero.dev/challenge/inclusive-range |
I think the depth limit is not the problem. I think the problem is that TS is not able to infer and limit the types correctly in our use case. We need to somehow make sure that the type of the next action matches the output of the previous action. |
Current behavior
pipe
method can only accept up to 9 pipe items because of its type signature.It's enough in most cases, but could be frustrating in some rare cases.
Such as a very long pipeline:
Or build pipe items dynamically:
Expected behavior
Have a fallback override that can accept unlimited items.
Inexact type inference is acceptable if difficulty exists. e.g.
unknown
output type.BTW, I am happy to contribute codes but was facing difficulties in writing types.
If someone can give a hand I will raise a pull request soon.
The text was updated successfully, but these errors were encountered: