Validation
Validation is the systematic process of verifying data for accuracy and integrity. This not only involves checking if the data type aligns with the expected type, but also whether any additional predefined constraints are satisfied.
Validation becomes paramount when dealing with data from uncertain or untrusted sources. An "uncertain" source is one where the types or contents of the data are unpredictable, potentially taking any value during runtime. Typical examples include user inputs, data from HTTP requests (like query parameters or the body), CLI arguments, or files that are read into a program. Such data is inherently risky as incorrect types or values can cause program failures, or even introduce security vulnerabilities.
For instance, if a variable is expected to store a number, validating to ensure that it actually contains a numeric value is crucial. A mismatch could lead to unexpected crashes or security breaches.
When designing an HTTP route controller for example, one must prioritize the validation of all user inputs, be it through query parameters, the request body, or other means. Particularly in environments utilizing TypeScript, it's vital to steer clear of type casts. These casts can be misleading and introduce fundamental security risks.
app.post('/user', function(request) { const limit = request.body.limit as number; });
A frequently encountered error in coding involves type casts that don't offer runtime security. For instance, if you type cast a variable as a number but a user inputs a string, the program is misled to operate as if the string is a number. Such oversights can cause system crashes or pose serious security threats. To mitigate these risks, developers can leverage validators and type guards. Additionally, serializers can play a role in converting variables, like converting 'limit' to a number. Further insights on this topic can be found in the section on Serialization.
Validation isn't just an option; it's an integral component of robust software design. It's always prudent to err on the side of caution: better to validate excessively than regret insufficient checks later. Deepkit understands this importance, offering a plethora of validation tools. What's more, its high-performance design ensures minimal impact on execution times. As a guiding principle, employ comprehensive validation to safeguard your application, even if it feels redundant at times.
Many of Deepkit's components, including the HTTP router, the RPC abstraction, and even the database abstraction, come with embedded validation systems. These mechanisms are automatically triggered, often eliminating the need for manual intervention.
For a comprehensive understanding of when and how automatic validation occurs, refer to specific chapters (CLI, HTTP, RPC, Database). Familiarize yourself with the necessary constraints and data types. Properly defined parameters can unlock Deepkit's automated validation potential, reducing manual labor and ensuring cleaner, more secure code.
Usage
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 workarounds.
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, is, assert } from '@deepkit/type'; const errors = validate<string>('abc'); //[] const errors = validate<string>(123); //[{code: 'type', message: 'Not a string'}] if (is<string>(value)) { // value is guaranteed to be a string } function doSomething(value: any) { assert<string>(value); //throws on invalid data // value is guaranteed to be 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 like 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.
Type Casts are Unsafe
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 a base case the program crashes and in the worst case a security vulnerability is created.
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 the 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
.
A restriction can be, for example, that a string must have a certain minimum or maximum length. These restrictions are added to the actual types via Type Annotations. There is a whole variety of annotations that can be used. Own annotations can be defined and used at will in case of extended needs.
import { MinLength } from '@deepkit/type'; type Username = string & MinLength<3>;
With &
any number of type annotations can be added to the actual type. The result, here username
, can then be used in all validation functions but also in other types.
import { is } from '@deepkit/type'; 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.
import { validate } from '@deepkit/type'; const errors = validate<Username>('xb'); //[{ code: 'minLength', message: `Min length is 3` }]
This 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 = /^\S+@\S+$/; type Email = string & Pattern<typeof emailRegexp> is<Email>('abc'); //false is<Email>('joe@example.com'); //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
Validation using a custom validator function. See next section Custom Validator for more information.
import { ValidatorError, Validate } from '@deepkit/type'; function startsWith(v: string) { return (value: any) => { const valid = 'string' === typeof value && value.startsWith(v); return valid ? undefined : new ValidatorError('startsWith', `Does not start with ${v}`); }; } type T = string & Validate<typeof startsWith, 'abc'>;
Pattern
Defines a regular expression as validation pattern. Usually used for E-Mail validation or more complex content validation.
import { Pattern } from '@deepkit/type'; const myRegExp = /[a-zA-Z]+/; type T = string & Pattern<typeof myRegExp>
Alpha
Validation for alpha characters (a-Z).
import { Alpha } from '@deepkit/type'; type T = string & Alpha;
Alphanumeric
Validation for alpha and numeric characters.
import { Alphanumeric } from '@deepkit/type'; type T = string & Alphanumeric;
Ascii
Validation for ASCII characters.
import { Ascii } from '@deepkit/type'; type T = string & Ascii;
Decimal<number, number>
Validation for string represents a decimal number, such as 0.1, .3, 1.1, 1.00003, 4.0, etc.
import { Decimal } from '@deepkit/type'; type T = string & Decimal<1, 2>;
MultipleOf
Validation of numbers that are a multiple of given number.
import { MultipleOf } from '@deepkit/type'; type T = number & MultipleOf<3>;
MinLength
Validation for min/max length for arrays or strings.
import { MinLength, MaxLength, MinMax } from '@deepkit/type'; type T = any[] & MinLength<1>; type T = string & MinLength<3> & MaxLength<16>; type T = string & MinMax<3, 16>;
Includes<'any'> Excludes<'any'>
Validation for an array item or sub string being included/excluded
import { Includes, Excludes } from '@deepkit/type'; type T = any[] & Includes<'abc'>; type T = string & Excludes<' '>;
Minimum
Validation for a value being minimum or maximum given number. Same as >=
and <=
.
import { Minimum, Maximum, MinMax } from '@deepkit/type'; type T = number & Minimum<10>; type T = number & Minimum<10> & Maximum<1000>; type T = number & MinMax<10, 1000>;
ExclusiveMinimum
Same as minimum/maximum but excludes the value itself. Same as >
and <
.
import { ExclusiveMinimum, ExclusiveMaximum } from '@deepkit/type'; type T = number & ExclusiveMinimum<10>; type T = number & ExclusiveMinimum<10> & ExclusiveMaximum<1000>;
Positive, Negative, PositiveNoZero, NegativeNoZero
Validation for a value being positive or negative.
import { Positive, Negative } from '@deepkit/type'; type T = number & Positive; type T = number & Negative;
BeforeNow, AfterNow
Validation for a date value compared to now (new Date)..
import { BeforeNow, AfterNow } from '@deepkit/type'; type T = Date & BeforeNow; type T = Date & AfterNow;
Simple regexp validation of emails via /^\S+@\S+$/
. Is automatically a string
, so no need to do string & Email
.
import { Email } from '@deepkit/type'; type T = Email;
integer
Ensures that the number is an integer in the correct range. Is automatically a number
, so no need to do number & integer
.
import { integer, uint8, uint16, uint32, int8, int16, int32 } from '@deepkit/type'; type T = integer; type T = uint8; type T = uint16; type T = uint32; type T = int8; type T = int16; type T = int32;
See Special types: integer/floats for more information
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 an arbitrary 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` }]);