Skip to content

Check action to infer return type from a passed typeguard #1089

@buzlo

Description

@buzlo

Here is a simple example. Let's say we have a typeguard that checks if a string starts with hash symbol:

export const checkStartsWithHash = (target: string): target is `#${string}` => target.startsWith('#')

What I would expect after passing that typeguard as an argument of check action is something like:

const input: string = '#fff'
const hashString = parse(
  pipe(string(), check(checkStartsWithHash)),
  input
) 
type Test = typeof hashString // `#${string}`

But instead of #${string} I get the initial input type with no consideration for the passed typeguard.
This could be previously bypassed with type casting: check<TypeToCast>(...), but with the latest update, it doesn't work anymore.

Is it something that you would consider adding?
Thanks!

Activity

fabian-hiller

fabian-hiller commented on Mar 20, 2025

@fabian-hiller
Owner

Hey! Thank you for reaching out! This is a duplicate of #986. Please take a look and feel free to provide feedback. In the meantime, here is a workaround with custom for you:

import * as v from 'valibot';

const Schema = v.custom<`#${string}`>(
  (input) => typeof input === 'string' && input.startsWith('#')
);
buzlo

buzlo commented on Mar 20, 2025

@buzlo
Author

Hey! Thank you for the answer and the provided workaround.

I'd like to emphasize though, that the case of startsWith typeguard is just an example, while the main question is if output type inference from check action is planned, in cases when the callback passed to such check action is actually a typeguard?

Or is the custom schema a preferred and intended way to achieve that?
Thanks again for the amazing job with Valibot!

fabian-hiller

fabian-hiller commented on Mar 21, 2025

@fabian-hiller
Owner

For now, I don't plan to support type guards with check, because check is a validation action, and transforming the output (even if it's just the type) requires a transformation action, and results in different behavior in some cases. But we can consider adding a new guard or typeGuard transformation action that supports this.

buzlo

buzlo commented on Mar 21, 2025

@buzlo
Author

Oh I see, fair enough.
A new guard action sounds good, I do think it would be a useful addition to Valibot.
Thanks for clarifying!

fabian-hiller

fabian-hiller commented on Mar 22, 2025

@fabian-hiller
Owner

Would guard be the best name in your opinion? Do you have any other ideas? Would you be interested in implementing this action? You can copy the check action and change its name and types.

buzlo

buzlo commented on Mar 23, 2025

@buzlo
Author

Sure, I'll be glad to participate. I haven't done it before before, but I'll see what I can do.
I do think guard is a well suited name, but if you think it might cause ambiguity, typeGuard is also a good choice in my opinion.

fabian-hiller

fabian-hiller commented on Mar 23, 2025

@fabian-hiller
Owner

Let's go with guard for now. You can also reach out on Discord in the contribution channel if your have question when creating a PR. I am happy to help!

matt-tingen

matt-tingen commented on May 17, 2025

@matt-tingen

Another workaround:

const guard = <T, U extends T>(predicate: (value: T) => value is U) =>
  [v.check(predicate), v.transform<T, U>((input) => input as U)] as const;

const hashStringSchema = v.pipe(v.string(), ...guard(checkStartsWithHash));

I'd also be interested in implementing an actual guard method, although I'm not sure I understand how it would work. From the comments here, it sounds like non-schema methods aren't allowed to be both validation and a type transformation. It seems like we would need to allow validations to narrow the output type, but not widen or change it to something disjoint.

I'm relatively new to valibot so I may have missed something. Is there an internal mechanism that would allow a single guard method?

matt-tingen

matt-tingen commented on May 17, 2025

@matt-tingen

Doing some more digging, I think I understand the issue with allowing validations to change their type, even if only narrowing. #986 (comment) helped solidify it for me.

I also now see rawTransform which allows both validation and transformation. I believe this allows for an implementation like this:

const guard = <T, U extends T>(
  predicate: (value: T) => value is U,
  message?: string,
) =>
  rawTransform<T, U>(({ addIssue, dataset, NEVER }) => {
    if (!predicate(dataset.value)) {
      addIssue({ message });

      return NEVER;
    }

    return dataset.value;
  });

const hashStringSchema = v.pipe(v.string(), guard(checkStartsWithHash));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

    Development

    Participants

    @matt-tingen@fabian-hiller@buzlo

    Issue actions

      Check action to infer return type from a passed typeguard · Issue #1089 · fabian-hiller/valibot