joi icon indicating copy to clipboard operation
joi copied to clipboard

Type level validation of schema

Open BackEndTea opened this issue 3 years ago • 1 comments

Support plan

  • is this issue currently blocking your project? no
  • is this issue affecting a production system? no

Context

  • node version: 14
  • module version: 17.4
  • environment node
  • used with standalone
  • any other relevant information: N/A

What problem are you trying to solve?

We're using Joi to validate a bunch of input. I want to make sure that every property is validated, as present, and the correct type.

For example i may have the following type, and its validation:

interface User {
  id: number,
  name: string
}

Joi.object<User>({
  id: Joi.number()
});

here i forgot to add validation for the name. Prehaps it was added later, and validation was forgotten. I'd like to have typescript errors when this happends

Do you have a new or modified API suggestion to solve the problem?

This is currently my homebrewn solution. One of the problems here is that i can't validate the generic types of Array, or if i use alternatives. ( If our object is of type Record<any, any> it can also be an empty array, which is why we use AlternativesSchema)


type StateSchema<T = object> = {
    [key in keyof T]: T[key] extends string
        ? StringSchema
        : T[key] extends number
        ? NumberSchema
        : T[key] extends boolean
        ? BooleanSchema
        : T[key] extends Array<any>
        ? ArraySchema
        : T[key] extends object
        ? ObjectSchema<T[key]>|AlternativesSchema
        : never
    ;
};

export const joiObject = <T>(input: StateSchema<T>): ObjectSchema<T> => Joi.object<T>(input);

BackEndTea avatar Jun 22 '21 17:06 BackEndTea

I'm currently looking for such solution too and I think that such typeguards should be included in library. IMO your solution is really great. And I hope I can help improve it.

  1. I think it's better to add a mappedTypeModifier to require all properties:
type StateSchema<T = object> = {
    [key in keyof T]-?: T[key] extends string
    // ...
  1. also there is unexpected behavior with nullable or optional parameters, so we can add Nullable check to the typeguard:
type NullableType<T> = undefined | null | T
type StateSchema<T = object> = {
    [key in keyof T]-?: 
        T[key] extends NullableType<string>
        ? Joi.StringSchema
        : T[key] extends NullableType<number>
        ? Joi.NumberSchema        
        // ...
 }
  1. also I know that a lot of people often reuse validation objects splitting them into variables, but in this case the current implementation of joiObject doesn't throw an error if there are extra properties, because it doesn't see object's type:
const objToValidate = {
    firstName: Joi.string(),
    lastName: Joi.string(),
    extraProperty: Joi.string(), // Error here
}

interface ObjType {
    firstName: string,
    lastName: string,
}

// No error
const typedSchemaPrevious = joiObjectPrevious<ObjType>(objToValidate) 
// Error -> "Argument of type ... is not assignable to ..."
const typedSchemaUpdatedV1 = joiObjectUpdatedV1<ObjType>()(objToValidate) 
// Error -> "Argument of type ... is not assignable to ..."
const typedSchemaUpdatedV2 = joiObjectUpdatedV2<ObjType, typeof objToValidate>(objToValidate) 

There are 2 variant of joiObjectUpdated function, because I'm not sure which one is better (IMO best option is to simplify it to use like yours (joiObjectPrevious<ObjType>(objToValidate)) but I don't know how:grin:)

and my final solution looks like:

type NullableType<T> = undefined | null | T;
type StateSchema<T = object> = {
  [key in keyof T]-?: T[key] extends NullableType<string>
    ? Joi.StringSchema
    : T[key] extends NullableType<number>
    ? Joi.NumberSchema
    : T[key] extends NullableType<bigint>
    ? Joi.NumberSchema
    : T[key] extends NullableType<boolean>
    ? Joi.BooleanSchema
    : T[key] extends NullableType<Array<any>>
    ? Joi.ArraySchema
    : T[key] extends NullableType<object>
    ? Joi.ObjectSchema<StateSchema<T[key]>> | Joi.AlternativesSchema
    : never;
};

type IncorrectProperties<P extends keyof any> = {
  [Key in P]: never;
};

type AllProperties<ResultType> = StateSchema<ResultType>;

type WithoutExtra<T, U extends AllProperties<T> = AllProperties<T>> = U & IncorrectProperties<Exclude<keyof U, keyof T>>;

const joiObjectUpdatedV1 =
    <T>() => <U extends StateSchema<T> = StateSchema<T>>(validationObject: WithoutExtra<T, U>) =>
    Joi.object(validationObject);

const joiObjectUpdatedV2 = 
    <T, U extends AllProperties<T> = AllProperties<T>>
    (validationObject: WithoutExtra<T, U>): Joi.ObjectSchema<T> => 
    Joi.object<T>(validationObject);

function joiObjectPrevious<T>(input: StateSchema<T>): Joi.ObjectSchema<T> {
  return Joi.object<T>(input);
}

And its usage:

const objToValidate = {
  firstName: Joi.string(),
  lastName: Joi.string(),
  extraProperty: Joi.string(), // Error here
};

interface ObjType {
  firstName: string;
  lastName: string;
}

// No error
const typedSchemaPrevious = joiObjectPrevious<ObjType>(objToValidate);
// Error -> "Argument of type ... is not assignable to ..."
const typedSchemaUpdatedV1 = joiObjectUpdatedV1<ObjType>()(objToValidate);
// Error -> "Argument of type ... is not assignable to ..."
const typedSchemaUpdatedV2 = joiObjectUpdatedV2<ObjType, typeof objToValidate>(
  objToValidate
);

petrenkoVitaliy avatar Jun 28 '21 03:06 petrenkoVitaliy

I'll close this issue as I think the basic mechanism is now implemented thanks to @petrenkoVitaliy and several other contributions. Any problem that may remain will receive better care in a separate issue.

Marsup avatar Sep 22 '22 11:09 Marsup