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

Runtypes-like .guard() #2413

Open
AlexXanderGrib opened this issue May 7, 2023 · 6 comments · May be fixed by #3862
Open

Runtypes-like .guard() #2413

AlexXanderGrib opened this issue May 7, 2023 · 6 comments · May be fixed by #3862

Comments

@AlexXanderGrib
Copy link

AlexXanderGrib commented May 7, 2023

I miss this feature from runtypes and i would like to implement it (making issue first, as CONTRIBUTING.md said)

My use case

I am recently got tasked with integrating multiple 3rd-party APIs, and i have chosen following pattern for it

class ExampleApiUnkownError extends Error { ... }
class ExampleApiError extends Error { ... }

const BaseResponse = z.object({
  response: z.custom((value) => value !== undefined)
})

type BaseResponse = z.static<typeof BaseResponse>; 

const ErrorResponse = z.object({
  error: z.object({ ... })
})

class ExampleApiClient {
  async request({...}): Either<ExampleApiError | ExampleApiUnknownError, BaseResponse> {
    try {
      const response = await fetch(...);
      const json = await response.json();
      
      // And here i would like to 
      // 1. Check if json matches BaseResponse -> return early
      // 2. Check if json matches ErrorResponse -> make ExampleApiError from it and return early
      // 3. return left(new ExampleApiUnkownError("...", { cause: { response, json } })); if nothing matched
      
      // In my opinion, both .parse and .safeParse are too cumbersome, cause
      // .parse requires error handling
      // .safeParse requires additional variable to work correctly
      
      // I prefer doing something like this
      
      if (BaseResponse.guard(json)) {
        return right(json.response);
      }
      
      if(ErrorResponse.guard(json)) {
        return left(ExampleApiError.fromJSON(json));
      }
      
      return left(new ExampleApiUnkownError("...", { response, json }));
      
    } catch(error) {
      return left(new ExampleApiUnkownError("...", { cause: error }));
    }    
  }
}
@AlexXanderGrib AlexXanderGrib changed the title Runtypes-like .check() Runtypes-like .guard() May 7, 2023
@stale
Copy link

stale bot commented Aug 6, 2023

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale No activity in last 60 days label Aug 6, 2023
@chriskuech
Copy link

This would be so helpful. This feels very boilerplatey and avoidable with such a feature:

const result = schema.safeParse(value);

if (result.success) {
  const data = result.data;
  
  // guarded code;
}

@stale stale bot removed the stale No activity in last 60 days label Aug 21, 2023
@am-burban
Copy link

You could just add a little helper function:

export const isValid = <Output>(
  schema: z.ZodType<Output>,
  data: unknown
): data is Output => schema.safeParse(data).success;

This allows you to use it as you intend to:

const schema = zod.number();
const data = JSON.parse("1");

if (isValid(schema, data)) {
  // data is a number here
}

RobinTail added a commit to RobinTail/express-zod-api that referenced this issue Jan 10, 2024
I was dreaming about it.
Unfortunately `zod` does not provide any way to make a custom or branded
or third-party schema that can be identified as one programmatically.
Developer of `zod` also does not want to make any method to store
metadata in schemas, instead, recommends to wrap schemas in some other
structures, which is not suitable for the purposes of `express-zod-api`.
There are many small inconvenient things in making custom schema
classes, that I'd like to replace into native methods, and use
`withMeta` wrapper for storing proprietary identifier, so the generators
and walkers could still handle it.

Related issues:

```
colinhacks/zod#1718
colinhacks/zod#2413
colinhacks/zod#273
colinhacks/zod#71
colinhacks/zod#37
```

PR I've been waiting for months to merged (programmatically
distinguishable branding):

```
colinhacks/zod#2860
```
@psychedelicious
Copy link

psychedelicious commented May 7, 2024

Related: #293, #430

It would be really handy to have a type guard function be created for each schema automatically.

This works:

// zod.d.ts
import 'zod';

declare module 'zod' {
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
  export interface ZodType<Output = any> {
    is(val: unknown): val is Output;
  }
}

// in the entrypoint
import { assert } from 'tsafe';
import { z } from 'zod';

// fail loudly if upstream adds a conflicting method
assert(!Object.hasOwn(z.ZodType.prototype, 'is'));

z.ZodType.prototype.is = function (val: unknown): val is z.infer<typeof this> {
  return this.safeParse(val).success;
};

// in the app
const zMySchema = z.string()
myArray.filter(zMySchema.is)

Could someone who knows what they are doing advise if this is a Bad Idea? I feel like it might be a Bad Idea.

@kpozin
Copy link

kpozin commented May 9, 2024

Note that your type guard is checking if val is of the schema's input type, but claiming that val is of the schema's output type.

const MySchema = z.coerce.string();

assert(MySchema.is(5)); // passes, but shouldn't

See the docs on z.infer, z.input, and z.output.

@oberbeck oberbeck linked a pull request Nov 19, 2024 that will close this issue
1 task
@oberbeck
Copy link

I’ve added type guards in #3862 for cases where Output extends Input, as I believe they are safe in those scenarios.

@kpozin, your example got me thinking, and I realized coercion should influence the Input type as well. To address this, I’ve adjusted the coercion implementations to ensure they impact Input, making the guards more reliable in those cases. Feedback is welcome!

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

Successfully merging a pull request may close this issue.

6 participants