Validation
Validation is the process of checking data for correctness. Correctness is given if the type is the correct one and additional defined constraints are fulfilled. Deepkit generally distinguishes between type validation and the validation of additional constraints.
Validation is used whenever data comes from a source that is considered uncertain. Uncertain means that no guaranteed assumptions can be made about the types or contents of the data, and thus the data could have literally any value at runtime. For example, data from user input is generally not considered secure. Data from an HTTP request (query parameter, body), CLI arguments, or a read-in file must be validated. If a variable is declared as a number, there must also be a number in it, otherwise the program may crash or a security hole may occur.
In a controller of an HTTP route, for example, the top priority is to check every user input (query parameter, body). Especially in the TypeScript environment, it is important not to use type casts, as they are fundamentally insecure.
app.post('/user', function(request) {
const limit = request.body.limit as number;
});
This often seen code is a bug that can lead to a program crash or a security vulnerability because a type cast as number
was used that does not provide any security at runtime. The user can simply pass a string as limit
and the program would then work with a string in limit
, although the code is based on the fact that it must be a number. To maintain this security at runtime there are validators and type guards. Also, a serializer could be used to convert limit
to a number. More information about this can be found in Serialization.
Validation is an essential part of any application and it is better to use it once too often than once too little. Deepkit provides many validation options and has a high-performance implementation, so in most cases there is no need to worry about execution time. Use as much validation as possible, in case of doubt once more, to be on the safe side.
In doing so, many components of Deepkit such as the HTTP router, the RPC abstraction, but also the database abstraction itself have validation built in and is performed automatically, so in many cases it is not necessary to do this manually.
In the corresponding chapters (CLI, HTTP, RPC, Database) it is explained in detail when a validation happens automatically. Make sure that you know where restrictions or types have to be defined and don’t use any
to make these validations work well and safely automatically. This can save you a lot of manual work to keep the code clean and safe.
Use
The basic function of the validator is to check a value for its type. For example, whether a value is a string. This is not about what the string contains, but only about its type. There are many types in Typescript: string, number, boolean, bigint, objects, classes, interface, generics, mapped types, and many more. Due to Typescript’s powerful type system, a large variety of different types are available.
In JavaScript itself, primitive types can be parsed with the typeof
operator. For more complex types like interfaces, mapped types, or generic set/map this is not so easy anymore and a validator library like @deepkit/type
becomes necessary. Deepkit is the only solution that allows to validate all TypesScript types directly without any detours.
In Deepkit, type validation can be done using either the validate
, is
, or assert
function.
The function is
is a so-called type guard and assert
is a type assertion. Both will be explained in the next section.
The function validate
returns an array of found errors and on success an empty array. Each entry in this array describes the exact error code and the error message as well as the path when more complex types like objects or arrays are validated.
All three functions are used in roughly the same way. The type is specified or referenced as the first type argument and the data is passed as the first function argument.
import { validate } from '@deepkit/type';
const errors = validate<string>('abc'); //[]
const errors = validate<string>(123); //[{code: 'type', message: 'Not a string'}]
If you work with more complex types like classes or interfaces, the array can also contain several entries.
import { validate } from '@deepkit/type';
interface User {
id: number;
username: string;
}
validate<User>({id: 1, username: 'Joe'}); //[]
validate<User>(undefined); //[{code: 'type', message: 'Not a object'}]
validate<User>({});
//[
// {path: 'id', code: 'type', message: 'Not a number'}],
// {path: 'username', code: 'type', message: 'Not a string'}],
//]
The validator also supports deep recursive types. Paths are then separated with a dot.
import { validate } from '@deepkit/type';
interface User {
id: number;
username: string;
supervisor?: User;
}
validate<User>({id: 1, username: 'Joe'}); //[]
validate<User>({id: 1, username: 'Joe', supervisor: {}});
//[
// {path: 'supervisor.id', code: 'type', message: 'Not a number'}],
// {path: 'supervisor.username', code: 'type', message: 'Not a string'}],
//]
Take advantage of the benefits that TypeScript offers you. For example, more complex types such as a user
can be reused in multiple places without having to declare it again and again. For example, if a user
is to be validated without its id
, TypeScript utitilies can be used to quickly and efficiently create derived subtypes. Very much in the spirit of DRY (Don’t Repeat Yourself).
type UserWithoutId = Omit<User, 'id'>;
validate<UserWithoutId>({username: 'Joe'}); //valid!
Deepkit is the only major framework that has the ability to access TypeScripts types in this way at runtime. If you want to use types in frontend and backend, types can be swapped out to a separate file and thus imported anywhere. Use this option to your advantage to keep the code efficient and clean.
A type cast (contrary to type guard) in TypeScript is not a construct at runtime, but is only handled in the type system itself. It is not a safe way to assign a type to unknown data.
const data: any = ...;
const username = data.username as string;
if (username.startsWith('@')) { //might crash
}
The as string
code is not safe. The variable data
could have literally any value, for example {username: 123}
, or even {}
, and would have the consequence that username
is not a string, but something completely different and therefore the code username.startsWith('@')
will lead to an error, so that in the worst case the program crashes. To guarantee at runtime that data
here has a property username
with the type string, type-guards must be used.
Type guards are functions that give TypeScript a hint about what type the passed data is guaranteed to have at runtime. Armed with this knowledge, TypeScript then "narrows" the type as the code progresses. For example, any
can be made into a string, or any other type in a safe way.
So if there is data of which the type is not known (any
or unknown
), a type guard helps to narrow it down more precisely based on the data itself. However, the type guard is only as safe as its implementation. If you make a mistake, this can have severe consequences, because fundamental assumptions suddenly turn out to be untrue.
Type-Guard
A type guard on the above used type User
could look in simplest form as follows. Note that the above explained special features with NaN are not part here and thus this type guard is not quite correct.
function isUser(data: any): data is User {
return 'object' === typeof data
&& 'number' === typeof data.id
&& 'string' === typeof data.username;
}
isUser({}); //false
isUser({id: 1, username: 'Joe'}); //true
A type guard always returns a Boolean and is usually used directly in an If operation.
const data: any = await fetch('/user/1');
if (isUser(data)) {
data.id; //can be safely accessed and is a number
}
Writing a separate function for each type guard, especially for more complex types, and then adapting it every time a type changes is extremely tedious, error-prone, and not efficient. Therefore, Deepkit provides the function is
, which automatically provides a Type-Guard for any TypeScript type. This then also automatically takes into account special features such as the above-mentioned problem with NaN. The function is
does the same as validate
, but instead of an array of errors it simply returns a boolean.
import { is } from '@deepkit/type';
is<string>('abc'); //true
is<string>(123); //false
const data: any = await fetch('/user/1');
if (is<User>(data)) {
//data is guaranteed to be of type User now
}
A pattern that can be found more often is to return an error directly in case of incorrect validation, so that subsequent code is not executed. This can be used in various places without changing the complete flow of the code.
function addUser(data: any): void {
if (!is<User>(data)) throw new TypeError('No user given');
//data is guaranteed to be of type User now
}
Alternatively, a TypeScript type assertion can be used. The assert
function automatically throws an error if the given data does not validate correctly to a type. The special signature of the function, which distinguishes TypeScript type assertions, helps TypeScript to automatically narrow the passed variable.
import { assert } from '@deepkit/type';
function addUser(data: any): void {
assert<User>(data); //throws on invalidate data
//data is guaranteed to be of type User now
}
Here, too, take advantage of the benefits that TypeScript offers you. Types can be reused or customized using various TypeScript functions.
Error Reporting
The functions is
, assert
and validates
return a boolean as result. To get exact information about failed validation rules, the validate
function can be used. It returns an empty array if everything was validated successfully. In case of errors the array will contain one or more entries with the following structure:
interface ValidationErrorItem {
/**
* The path to the property. Might be a deep path separated by dot.
*/
path: string;
/**
* A lower cased error code that can be used to identify this error and translate.
*/
code: string,
/**
* Free text of the error.
*/
message: string,
}
The function receives as first type argument any TypeScript type and as first argument the data to validate.
import { validate } from '@deepkit/type';
validate<string>('Hello'); //[]
validate<string>(123); //[{code: 'type', message: 'Not a string', path: ''}]
validate<number>(123); //[]
validate<number>('Hello'); //[{code: 'type', message: 'Not a number', path: ''}]
Complex types such as interfaces, classes, or generics can also be used.
import { validate } from '@deepkit/type';
interface User {
id: number;
username: string;
}
validate<User>(undefined); //[{code: 'type', message: 'Not an object', path: ''}]
validate<User>({}); //[{code: 'type', message: 'Not a number', path: 'id'}]
validate<User>({id: 1}); //[{code: 'type', message: 'Not a string', path: 'username'}]
validate<User>({id: 1, username: 'Peter'}); //[]
Constraints
In addition to checking the types, other arbitrary constraints can be added to a type. The validation of these additional content constraints is done automatically after the types themselves have been validated. This is done in all validation functions like validate
, is
, and assert
. For example, a constraint can be that a string must have a certain minimum or maximum length.
These constraints are added to the actual types via the type decorators. There is a whole variety of decorators that can be used. Own decorators can be defined and used at will in case of extended needs.
type Username = string & MinLength<3>;
With &
any number of type decorators can be added to the actual type. The result, here username
, can then be used in all validation functions but also in other types.
is<Username>('ab'); //false, because minimum length is 3
is<Username>('Joe'); //true
interface User {
id: number;
username: Username;
}
is<User>({id: 1, username: 'ab'}); //false, because minimum length is 3
is<User>({id: 1, username: 'Joe'}); //true
The function validate
gives useful error messages coming from the constraints.
const errors = validate<Username>('xb');
//[{ code: 'minLength', message: `Min length is 3` }]
These information can be represented for example wonderfully also at a form automatically and be translated by means of the code
. Through the existing path for objects and arrays, fields in a form can filter out and display the appropriate error.
validate<User>({id: 1, username: 'ab'});
//{ path: 'username', code: 'minLength', message: `Min length is 3` }
An often useful use case is also to define an email with a RegExp constraint. Once the type is defined, it can be used anywhere.
export const emailRegexp = /^\[email protected]\S+$/;
type Email = string & Pattern<typeof emailRegexp>
is<Email>('abc'); //false
is<Email>('[email protected]'); //true
Any number of constraints can be added.
type ID = number & Positive & Maximum<1000>;
is<ID>(-1); //false
is<ID>(123); //true
is<ID>(1001); //true
Constraint Types
Validate<typeof MyValidator>
Validation using a custom validator function. See next section Custom Validator for more information.
type T = string & Validate<typeof myValidator>
Pattern<typeof MyRegexp>
Defines a regular expression as validation pattern. Usually used for email validation or more complex content validation.
const myRegExp = /[a-zA-Z]+/;
type T = string & Pattern<typeof myRegExp>
Decimal<number, Number>
Validation for string represents a decimal number, such as 0.1, .3, 1.1, 1.00003, 4.0, etc.
type T = string & Decimal<1, 2>;
MultipleOf<number>
Validation of numbers that are a multiple of given number.
type T = number & MultipleOf<3>;
MinLength<number>, MaxLength<number>
Validation for min/max length for arrays or strings.
type T = any[] & MinLength<1>;
type T = string & MinLength<3> & MaxLength<16>;
Includes<'any'> Excludes<'any'>
Validation for an array item or sub string being included/excluded
type T = any[] & Includes<'abc'>;
type T = string & Excludes<' '>;
Minimum<number>, Maximum<number>
Validation for a value being minimum or maximum given number. Same as >=
and <=
.
type T = number & Minimum<10>;
type T = number & Minimum<10> & Maximum<1000>;
ExclusiveMinimum<number>, ExclusiveMaximum<number>
Same as minimum/maximum but excludes the value itself. Same as >
and <
.
type T = number & ExclusiveMinimum<10>;
type T = number & ExclusiveMinimum<10> & ExclusiveMaximum<1000>;
Positive, Negative, PositiveNoZero, NegativeNoZero
Validation for a value being positive or negative.
type T = number & Positive;
type T = number & Negative;
BeforeNow, AfterNow
Validation for a date value compared to now (new Date)…
type T = Date & BeforeNow;
type T = Date & AfterNow;
Simple regexp validation of emails via /^\[email protected]\S+$/
. Is automatically a string
, so no need to do string & Email
.
type T = Email;
Custom Validator
If the built-in validators are not sufficient, custom validation functions can be created and used via the Validate
decorator.
import { ValidatorError, Validate, Type, validates, validate }
from '@deepkit/type';
function titleValidation(value: string, type: Type) {
value = value.trim();
if (value.length < 5) {
return new ValidatorError('tooShort', 'Value is too short');
}
}
interface Article {
id: number;
title: string & Validate<typeof titleValidation>;
}
console.log(validates<Article>({id: 1})); //false
console.log(validates<Article>({id: 1, title: 'Peter'})); //true
console.log(validates<Article>({id: 1, title: ' Pe '})); //false
console.log(validate<Article>({id: 1, title: ' Pe '})); //[ValidationErrorItem]
Note that your custom validation function is executed after all built-in type validators have been called. If a validator fails, all subsequent validators for the current type are skipped. Only one failure is possible per type.
Generic Validator
In the Validator function the type object is available which can be used to get more information about the type using the validator. There is also a possibility to define any validator option that must be passed to the validate type and makes the validator configurable. With this information and its parent references, powerful generic validators can be created.
import { ValidatorError, Validate, Type, is, validate }
from '@deepkit/type';
function startsWith(value: any, type: Type, chars: string) {
const valid = 'string' === typeof value && value.startsWith(chars);
if (!valid) {
return new ValidatorError('startsWith', 'Does not start with ' + chars)
}
}
type MyType = string & Validate<typeof startsWith, 'a'>;
is<MyType>('aah'); //true
is<MyType>('nope'); //false
const errors = validate<MyType>('nope');
//[{ path: '', code: 'startsWith', message: `Does not start with a` }]);