Implementing a custom validation decorator is simple by using the generateValidationDecorator
function. This function handles storing the metadata required so that @ValidateParams
and @ValidateAccessor
know what validations to execute. This function is used in all validation decorators exported by the runtime-data-validation
package.
The following code comes from the test suite, test/src/custom.ts
in the repository.
import {
ValidateParams, ValidateAccessor, generateValidationDecorator,
validators
} from 'runtime-data-validation';
Import a few functions from the package, including generateValidationDecorator
. The validators
field contains simple validation functions which return true
or false
.
type CustomType = {
flag1: boolean;
speed: number;
title?: string;
};
Define a data type that we will validate. In TypeScript it is not possible to attach decorators to such a type. But we could use this type as an object field, or as a method parameter.
class CustomValidationExample {
#custom: CustomType;
@ValidateAccessor<CustomType>()
@IsCustom()
set custom(nc: CustomType) { this.#custom = nc; }
get custom() { return this.#custom; }
}
const cve = new CustomValidationExample();
This is a simple class which uses the type in a property. There are set
and get
accessors for this property. It uses the type in the correct way, storing the value provided to set
into the private property.
Validation uses a decorator, @IsCustom
, that could be implemented like so:
const isCustomType = (value: any): boolean => {
if (typeof value !== 'object') {
return false;
}
// Both of these fields are required in
// the type definition
if (typeof value?.flag1 !== 'boolean'
|| typeof value?.speed !== 'number') {
return false;
}
// Speed must be a positive number
if (value.speed < 0) {
return false;
}
// This field is optional, and therefore can
// be undefined.
if (typeof value?.title !== 'undefined'
&& typeof value?.title !== 'string') {
return false;
}
// Be certain that `title` is correct
if (typeof value?.title !== 'undefined'
&& typeof value?.title === 'string') {
if (!validators.isAscii(value.title)
|| !validators.matches(value.title, /^[a-zA-Z0-9 ]+$/)) {
return false;
}
}
return true;
};
function IsCustom() {
return generateValidationDecorator(
isCustomType,
`Value :value: is not a CustomType`);
}
Each TypeScript decorator is implemented by a function. That means the IsCustom
function can be used as a decorator as @IsCustom()
. For a complete introduction see How to Use and Implement TypeScript Decorators
This decorator function follows the decorator factory pattern. The IsCustom
function itself is not a decorator function. Instead the decorator function is returned by generateValidationDecorator
.
This function takes two parameters:
- The validation function - these return
false
if the item fails validation, andtrue
if it is valid - A message to use in the thrown exception if validation fails. The marker
:value:
will receive the text resulting from callingutil.inspect(value)
.
In this case, isCustomType
can serve as a Type Guard function separately from being used in the validation decorator. Type guards are a pattern recommended by the TypeScript team to create functions like this which inspect objects and determine if the object matches a desired shape. The isCustomType
function is then used as the validation function.
In isCustomType
we demostrate using the validation functions in validators
for custom validation. Because CustomType
is not a class, we cannot attach decorators to its properties. But, validators.isAscii
and validators.matches
the implementation behind the @IsAscii
and @matches
decorators. That makes these functions equivalent to using the corresponding decorators.
The following tests produce either success or failure:
const cve = new CustomValidationExample();
// Success cases
cve.custom = {
flag1: true,
speed: 1000
};
cve.custom = {
flag1: false,
speed: -2000
};
// Failure cases
cve.custom = 'Fooo';
cve.custom = { };
cve.custom = {
flag1: 'true',
speed: '1000'
};
The success cases match the shape of CustomType, while the failure cases do not. For the failure cases an exception is thrown by the validation.
The TypeScript compiler will flag these at compile time. But what if your code is used from JavaScript where there is no compile time, nor compile time type checking?
Passing custom options to a custom validation decorator
Using the decorator factory pattern we can easily pass parameters to use in validation. This can be used for customizing the validation. In our example above, the speed
field might be limited to a specific range of speeds. We don't want to get a speeding ticket, after all.
function IsCustomRange(min: number, max: number) {
return generateValidationDecorator(
(value) => {
if (!isCustomType(value)) return false;
const ct = <CustomType>value;
if (ct.speed < min || ct.speed > max) return false;
return true;
},
`Value :value: is not a CustomType`);
}
This decorator function takes two values, min
and max
. The validation function first verifies that value matches the shape of CustomType. It then performs a range check on the speed
property, to make sure it is within the range specified.
To implement this in the class, add the following:
#customRange: CustomType;
@ValidateAccessor<CustomType>()
@IsCustomRange(0, 100)
set customRange(nc: CustomType) { this.#customRange = nc; }
get customRange() { return this.#customRange; }
Then, to test it, use cve.customRange
and assign different values to cve.customRange.speed
.
Using type predicates in type guard functions used for data validation
We described isCustomType
as a type guard function. The TypeScript documentation on Narrowing contains much discussion of the type guard concept. Type guard functions inspect an object, and determine whether it has the correct type.
A related TypeScript feature is the type predicate operator. Such a function looks like this:
const isCustomTypeGuard = (value: any): value is CustomType => {
...
};
This, again, is directly out of the test suite. The key is the return value, VARIABLE is TYPE
. This operator is essentially the same as boolean
and there is no difference in behavior between isCustomType
(returns boolean
) and isCustomTypeGuard
(returns type predicate).