class-validator icon indicating copy to clipboard operation
class-validator copied to clipboard

question: is there a way to ensure validators and types match?

Open nicolaschambrier opened this issue 2 years ago • 1 comments

We're comparing different schema validation libraries and one of my strongest concerns is the possibility to lie with class-validator. Here is an obviously stupid example, but it shows how we can have validators that make zero sense with the type, but will still wait for runtime to fail:

class TestClass {
  @IsOptional()
  @IsString()
  age: number

  @IsBoolean()
  lastName?: string

  @IsBoolean()
  firstName?: string
}

const tstObj1 = new TestClass()
tstObj1.age = 42
tstObj1.firstName = undefined
tstObj1.lastName = 'test'

expect(validateSync(tstObj1)).toEqual([
  {
    target: tstObj1,
    value: 42,
    property: 'age',
    children: [],
    constraints: { isString: 'age must be a string' },
  },
  {
    target: tstObj1,
    value: 'test',
    property: 'lastName',
    children: [],
    constraints: { isBoolean: 'lastName must be a boolean value' },
  },
  {
    target: tstObj1,
    value: undefined,
    property: 'firstName',
    children: [],
    constraints: { isBoolean: 'firstName must be a boolean value' },
  },
])

In "real life" this happened with @IsOptional() which had been added where it should have not, and was not here where it should have. But I guess in bigger models it can happen on more sever cases.

How can I ensure the validators and the types always match, at least for the basics?

What I expected here

Typing errors preventing compilation:

  • Cannot use @IsOptional() @IsString() on "age" as string | null | undefined is not compatible with number
  • Cannot use @IsBoolean() on "lastName" as boolean is not compatible with string | undefined
  • Cannot use @IsBoolean() on "firstName" as boolean is not compatible with string | undefined

Maybe it's just a limitation of the decorators, I still hope it can be achieved with strictier typing on validators.

nicolaschambrier avatar Feb 13 '23 13:02 nicolaschambrier

Does it make sense to invest time in this change?

I decided to create my own wrapper decorators and wait for lib to switch away from legacy decorators first. Suspect that a lot of type safety will be solved when class-validator will switch away from legacy decorators.

export type OurPropertyDecorator<T, K extends string | symbol, KType> = (
  target: K extends keyof T
    ? T[K] extends KType | undefined
      ? T
      : never
    : never,
  propertyKey: K,
) => void;


function IsOurEnum<T extends object, K extends string | symbol>({
  required,
}: {
  required: boolean;
}): OurPropertyDecorator<T, K, OurEnum> {
  return function (target, propertyKey): void {
    Expose()(target, propertyKey);
    IsEnum(Support)(target, propertyKey);
    ApiProperty({
      enum: OurEnum,
      example: 'example-value',
      required,
    })(target, propertyKey);
    if (required) {
      IsDefined()(target, propertyKey);
    } else {
      IsOptional()(target, propertyKey);
    }
  };
}

class A {
  @IsOurEnum
  k: OurEnum
}

karlismelderis-mckinsey avatar May 29 '23 14:05 karlismelderis-mckinsey