class-validator
class-validator copied to clipboard
question: is there a way to ensure validators and types match?
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" asstring | null | undefined
is not compatible withnumber
- Cannot use
@IsBoolean()
on "lastName" asboolean
is not compatible withstring | undefined
- Cannot use
@IsBoolean()
on "firstName" asboolean
is not compatible withstring | undefined
Maybe it's just a limitation of the decorators, I still hope it can be achieved with strictier typing on validators.
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
}