-
-
Notifications
You must be signed in to change notification settings - Fork 888
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
Feature request: type-aware validation functions #736
Comments
If it's helpful (or anyone else is interested), here's what we're doing to work around this issue currently: export function buildValidator<T>(ref: string): ValidateFunction<T> {
// we previously used `addSchema` to define `my-schema.json`
return ajv.compile({
$ref: `my-schema.json#/definitions/${ref}`
});
}
export interface ValidateFunction<T> extends Ajv.ValidateFunction {
_t?: T; // stop linter from complaining about unused T variable
}
function isValid<T>(
validator: ValidateFunction<T>,
candidate: any
): candidate is T {
return validator(candidate) === true;
}
const validateFoo = buildValidator<{foo: number}>("asdf");
const y: any = {};
if(isValid(validateFoo, y) {
y // is treated as a `{foo: number}` here.
} |
Just to clarify, you want generated code to be typescript? |
Probably not... |
Hi @epoberezkin, I'm not looking for AJV to generate types or for any change to the run-time behavior of AJV or even the existing TypeScript API. I'm hoping we can enhance AJV's built-in typescript types to enable users opt into better leveraging AJV as a way to generate type testing functions, which is ultimately what JSON schema is all about. TypeScript supports a few features that are particularly relevant here:
The two changes I suggested in my original message in this issue show an example of how that could be structured. With those changes, I'd expect to be able to do the following: const isPerson = ajv.compile<Person>("person.schema.json");
// isPerson is a type guard that proves it's argument is a Person.
// The return type of isPerson(data) is `data is Person`
if (isPerson(someData)) {
// TypeScript type checks `someData` as a `Person` in this block
alert(`${someData.firstName} ${someData.lastName}`);
} else {
// TypeScript knows that `someData` is _not_ a person in this block;
} I believe these changes can be made a) without breaking the current TypeScript API, b) without changing any runtime behavior in AJV, and c) without adding additional machinery to e.g. generate types. This change is merely about better modeling the existing behavior of AJV with TypeScript's type system. It's worth noting that there's nothing stopping users from inventing this machinery themselves. The workaround I posted in my follow-up message provides all of the same capabilities. However, this does seem like it should be a part of AJV, as the primary reason for using AJV is to check that data matches the set of contstraints for some type, so they can know they have a valid instance of that type. This is exactly what a type guard in TypeScript are for, so this seems like a more natural way to model validation functions in TypeScript. Does this clarify the nature of the request? I'm happy to provide additional clarification as necessary. Also, do you have concerns about making such a change? From my outside perspective, it seems like a win for users that is in the spirit of what AJV is already designed to do, but perhaps there are potential concerns I haven't considered. |
Looks like it won’t hurt... cc @blakeembrey |
@epoberezkin This should be good and all the parts were well documented above by @dcolthorp 👍 (just needs the generic param and default it to |
I had this exact idea yesterday, so I'm glad I checked the issues first and found this. I agree with @dcolthorp that this seems like something that should be able to be done with no runtime changes or additional layers. But so far, I've been a bit confounded by the existing return type of Another complication is that, as I read this at least, type guards need to be on the function declaration proper, and not just on the type definition due to the fact that they're contextually typed. I'm wondering if this is just a matter of generating some JSDoc or commented type annotations in the compiled function? That would be a slight runtime change, but I don't think it would be noticeable. Here's a TypeScript Playground link where I noodled around on some things. I did manage to get something kinda working with a combination of a default generic parameter and conditional types, but it's kind of complicated and may not be what we want at the end of the day. Here's the final form of that experiment: type Validator4<T=void> =
T extends void ? (data: any) => boolean :
T extends Promise<any> ? (data: any) => T :
(data: any) => data is T;
// Example guard functions
const isStringBool = <Validator4>((data: any) => typeof data === "string");
const isString = <Validator4<string>>((data: any) => typeof data === "string");
const eventuallyValidate = <Validator4<Promise<string>>>
((data: any) => typeof data === "string" ? Promise.resolve(data) : Promise.reject()) Something like this might work for A simpler interface would probably be easier to type check, so I might be leaning toward a smaller wrapper for TypeScript users with fewer union return types, either as an alternate interface or maybe a separate package. But either way, having the compiled code generate an annotation (so that it could be more easily imported) would probably be a good idea (although that's probably a different ticket). |
I've been working on https://github.com/ostrowr/ts-json-validator the last couple days, which does pretty much exactly this, and happens to use ajv behind the scenes anyway. If a maintainer would be interested, I'm happy to port a bunch of that work over to ajv, or implement a plugin that implements these types. |
@ostrowr your library looks great and the clear, concise documentation is refreshing. thank you thank you thank you. (now to give it a try...) |
@ostrowr implementing plugin is better as Ajv is not a typescript library. |
@Janpot Unfortunately, as it currently stands it would probably require some changes to the runtime, since each sub-schema (currently) needs to be marked using "createSchema." |
@dcolthorp mere 2 years later consider to include it in upcoming v7 release :) |
@ostrowr now would be the great time to get some of the ideas from ts-json-validator into ajv - it's now re-written into typescript (v7-alpha branch). I plan to release v7 beta end of September, it would be great if you could have a look. Thank you. |
I've never seen that many thumbs-up on a single issue in Ajv - thanks a lot for supporting this feature! Validating type guards are now in v7-alpha branch, coming to npm soon. I've also added So if you write JSON schemas manually rather than generate them from interfaces, e.g. when typescript interface is not expressive enough to define dependencies or when you want to add any additional restrictions on top of the interface, this type could simplify it and help avoiding mistakes and ensuring correctness of the schema. Check it out here: type: https://github.com/ajv-validator/ajv/blob/v7-alpha/lib/types/json-schema.ts Any feedback or suggestions would be really appreciated :) |
I probably won't have time to make any serious contributions before the end of September, but I'll certainly take a look and see where I can help ASAP! |
What version of Ajv you are you using?
6.2.1
What problem do you want to solve?
Make better use of TypeScript types with AJV.
We're using json-schema-to-typescript to generate typescript types from our JSON schemas. To use these, we're currently creating type guard functions that use the schema to validate a value as an unknown shape. If the schema validates, we know that the typescript type is accurate. Right now this is a manual process, because AJV doesn't provide a mechanism to tie a typescript type to a ValidateFunction when compiling a schema.
What do you think is the correct solution to problem?
Provide a way to
compile<SomeType>(mySomeTypeSchema)
which returns aValidateFunction<T>
, which might look something like(data: any,...) => data is T
. Using this validate function in an if statement would then let TypeScript automatically castdata
toT
within the then branch without an explicit cast.This might look something like
ValidateFunction
toValidateFunction<T=any>
and the return type todata is T
compile
and other functions that returnValidateFunction
to e.g.compile<T=any>(schema: object | boolean): ValidateFunction<T>
;The complication is the return type of
data is T | Thenable<any>
. Since TypeScript doesn't know which you have statically, this doesn't get you all the way there. It does work if you remove theThenable<any>
return type option. Solving this will require either a more nuanced modelling of the types – perhaps dividingValidateFunction
fromAsyncValidateFunction
or providing a few overloads forValidateFunction
may solve it, though I admit I haven't looked that closely enough to know for sure.Will you be able to implement it?
I'd be happy to help translate example use cases in to type signatures. Time to get deep enough familiarity with AJV is the main limiting factor to putting making the changes entirely myself.
The text was updated successfully, but these errors were encountered: