Hi, i've been using Joi for object validation with typescript and I believe we can improve the type definitions using typescript 2.8 features such as conditional types.
Problem & motivating example:
Consider you have a data structure represented with an Interface that you want to create using user input.
interface UserProfile {
name: string;
dateOfBirth: Date;
address: Address;
}
interface Address {
county: string;
}
Using Joi you need to define a schema for this data like follows
const schema = Joi.object({
name: Joi.string().required(),
dateOfBirth: Joi.string().required(),
address: Joi.object({
country: Joi.string(),
}),
});
And then you can validate user input.
One problem that stands out to me is that there is nothing to stop me from incorrectly defining the schema. In fact, in the above example i've incorrectly used dateOfBirth: Joi.string().required() where I actually needed to use Joi.date().required() to match the original interface.
Using conditional recursive mapped types in typescript 2.8 we can introduce typesafe schemas:
Below is a joi.d.ts augmentation to outline the approach
import * as Joi from 'joi';
declare module 'joi' {
// given a <T> we can use typescript 2.8 conditional types to map different
// primative types to Joi schemas
export type TypedSchemaLike<T> = T extends string ? Joi.StringSchema :
T extends number ? Joi.NumberSchema :
T extends Date ? Joi.DateSchema :
T extends boolean ? Joi.BooleanSchema :
T extends Object ? TypedObjectSchema<T> :
never;
// given some <T> (i.e. some object) we want to use typescript mapped types
// to convert the properties of the object into Joi schema types
export type TypedSchemaMap<T> = {
[P in keyof T]: TypedSchemaLike<T[P]>;
};
export interface TypedObjectSchema<T> extends ObjectSchema {}
// Joi.object can accept a type argument now. it is used to map typescript types
// to different Joi schema types, helping type safety overall.
export function object<T>(schema?: TypedSchemaMap<T>): TypedObjectSchema<T>;
}
With the above, we can then update our schema declaration to take advantage of the generic argument
const schema = Joi.object<UserProfile>({
name: Joi.string().required(),
dateOfBirth: Joi.string().required(), // <--- compilation error: Type StringSchema is not assignable to type DateSchema
address: Joi.object<Address>({
country: Joi.string(),
}),
});
I believe this is really helpful for making codebases more typesafe. We could even take this example further to introduce typesafety for non-optional properties and Joi's .required().
Additionally, we could improve the type safety of Joi functions such as validate:
// joi.d.ts
export function validate<T>(value: T, schema: TypedSchemaLike<T>): ValidationResult<T>;
// our code
Joi.validate(1, schema); // <--- compilation error: TypeObjectSchema<UserProfile> is not assignable to parameter of type NumberSchema
What do you all think? I'd be happy to submit a PR for this change or some subset of it if the full scope is too large.
Authors:
@Bartvds @laurence-myers @cglantschnig @broder @GaelMagnan @schfkt @rokoroku @dankraus @wanganjun @rafaelkallis @aconanlai
Yeah, I agree that Conditional Types from TS2.8 is awesome!
I think just adding conditional type to ObjectSchema is enough here.
To keep my Joi schema in sync with my TS interfaces, I ended up writing a library that lets you apply Joi validation using decorators. You define a dummy "validation class", which extends from an interface, and applies decorators on each property. (I don't know how well this works with TS 2.8's better class property initialisation checking.)
I have also experimented with the opposite approach to type mapping: mapping schemas to types/interfaces.
The issues I encountered are:
Going from type -> schema should be less problematic, but you might have issues mapping optional types and type unions.
I think your proposed functionality is highly valuable. But I'm not sure if it belongs in the type declarations. In theory, you _could_ validate any object against any schema - I'm not sure if we should be artificially limiting Joi's capabilities by making the type definition too strict. And if we can't support every feature available in Joi just through type mapping, there's a chance the type definitions will break someone's existing use of Joi.
However, I'd definitely like to see this implemented as a separate library. :smile:
Hey guys, I an working on a standalone library typesafe-joi. I used lots of conditional types to implement it. And with the help of TypeScript 3.0 tuple update, most of the essential APIs are well-typed now. But there are still some limitations.
Please let me know if you find it useful 馃槃
What is the progress on this? I think this is an awesome addition, especially since it is opt-in. Are there any requirements not met?
Same here @aimed. Any updates here?
Now that TS 4.0 is out, it should be possible to cover most of Joi's API with more typesafe definitions.
Most helpful comment
To keep my Joi schema in sync with my TS interfaces, I ended up writing a library that lets you apply Joi validation using decorators. You define a dummy "validation class", which extends from an interface, and applies decorators on each property. (I don't know how well this works with TS 2.8's better class property initialisation checking.)
I have also experimented with the opposite approach to type mapping: mapping schemas to types/interfaces.
The issues I encountered are:
Going from type -> schema should be less problematic, but you might have issues mapping optional types and type unions.
I think your proposed functionality is highly valuable. But I'm not sure if it belongs in the type declarations. In theory, you _could_ validate any object against any schema - I'm not sure if we should be artificially limiting Joi's capabilities by making the type definition too strict. And if we can't support every feature available in Joi just through type mapping, there's a chance the type definitions will break someone's existing use of Joi.
However, I'd definitely like to see this implemented as a separate library. :smile: