验证

Validation是检查数据正确性的过程。如果类型是正确的,并且满足了其他定义的限制,那么正确性就得到了。Deepkit通常区分类型验证和附加约束的验证。

只要数据来自被认为不安全的来源,就会使用验证。不确定意味着不能对数据的类型或内容做出有保障的假设,因此数据在运行时可能具有字面上的任何价值。 因此来自用户输入的数据通常不被认为是安全的。来自HTTP请求(查询参数、正文)、CLI参数或读入文件的数据必须被验证。如果一个变量被声明为数字,其中也必须有一个数字,否则程序会崩溃或出现安全漏洞。

例如,在一个HTTP路由的控制器中,首要任务是检查每个用户的输入(查询参数,正文)。特别是在TypeScript环境中,不要使用类型转换,因为它们从根本上是不安全的。

app.post('/user', function(request) {
    const limit = request.body.limit as number;
});

这段经常看到的代码是一个错误,可能导致程序崩溃或安全漏洞,因为使用了一个在运行时不提供任何安全保障的 "作为数字 "的类型铸造。用户可以简单地传递一个字符串作为`limit',然后程序就会在`limit’中工作,尽管代码的基础是它必须是一个数字。为了在运行时保持这种安全性,有验证器和类型保护器。另外,可以用一个序列化器将 "limit "转换为一个数字。关于这方面的更多信息,请参阅Serialization

验证是任何应用程序的重要组成部分,使用一次太频繁比一次太少好。Deepkit提供了许多验证选项,并有一个高性能的实现,所以在绝大多数情况下,不需要担心执行时间。

Deepkit的许多组件,如HTTP路由器、RPC抽象,以及数据库抽象本身,都内置了验证功能,并自动执行,因此在许多情况下,没有必要手动操作。 在相应的章节(CLI, HTTP, RPC, Database)中,详细解释了何时自动发生验证。确保你知道哪里需要定义限制或类型,不要使用`any`来使这些验证自动良好安全地工作。这可以为你节省大量的手工工作以保持代码的清洁和安全。

使用

验证器的基本功能是检查一个值的类型。例如,一个值是否是一个字符串。这不是关于字符串包含什么,只是它的类型。在Typescript中有很多类型:字符串、数字、布尔值、大数、对象、类、接口、泛型、映射类型等等。通过Typescript强大的类型系统,可以提供大量不同的类型。

在JavaScript本身,原始类型可以用`typeof`操作符进行解析。对于更复杂的类型,如接口、映射类型或通用集合/映射,这就不再那么容易了,因此需要一个验证器库,如`@deepkit/type`。Deepkit是唯一允许直接验证所有TypesScript类型而不走弯路的解决方案。

在Deepkit中,可以使用`validate`、`is`或`assert`函数进行类型验证。 函数`is`是一个所谓的类型保护,`assert`是一个类型断言。两者都将在下一节进行解释。 函数`validate`返回一个发现错误的数组,如果成功则返回一个空数组。这个数组中的每个条目都描述了确切的错误代码和错误信息以及路径,只要更复杂的类型,如对象或数组得到验证。

这三个函数的使用大致相同。类型被指定或引用为第一个类型参数,数据作为第一个函数参数被传递。

import { validate } from '@deepkit/type';

const errors = validate<string>('abc'); //[]
const errors = validate<string>(123); //[{code: 'type', message: 'Not a string'}]

当处理更复杂的类型如类或接口时,数组也可以包含几个条目。

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'}],
//]

验证器也支持深度递归类型。路径然后用一个点隔开。

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'}],
//]

利用TypeScript为你提供的好处。因此,更复杂的类型,如 "用户",可以在多个地方重复使用,而不必一次又一次地声明。例如,如果一个 "用户 "要在没有 "id "的情况下被验证,TypeScript的工具可以被用来快速有效地创建派生的子类型。为了保持DRY(Don’t Repeat Yourself)。

type UserWithoutId = Omit<User, 'id'>;

validate<UserWithoutId>({username: 'Joe'}); //valid!

Deepkit是唯一能够在运行时以这种方式访问TypeScripts类型的主要框架。如果你想在前端和后端使用类型,可以将类型外包给一个单独的文件,从而在任何地方都可以导入。

TypeScript中的类型转换(与类型保护相反)在运行时不是一个构造,而只是在类型系统本身中处理。

const data: any = ...;

const username = data.username as string;

if (username.startsWith('@')) { //might crash
}

代码 "as string "在此并不安全,它是为未知数据分配类型的一种方式。变量`data`实际上可以有任何值,例如`{username: 123}`,甚至`{}`,这将导致`username`不是一个字符串,而是完全不同的东西,因此代码`username.startingWith('@')将导致一个错误,所以在最坏的情况下,程序会崩溃。为了保证在运行时`data`有一个类型为String的属性`username,必须使用类型保护。

类型保护是给TypeScript一个提示,告诉它在运行时保证传递的数据有什么类型。掌握了这些知识,TypeScript就会随着代码的进展完善("缩小")该类型。 例如,any`可以以安全的方式变成字符串或其他类型。 因此,如果有数据的类型不知道(`any`或`unknown),类型保护有助于根据数据本身更精确地缩小范围。然而,类型保护只有在其实施时才是安全的。如果你犯了一个错误,这可能会产生严重的后果,因为基本的假设突然变成了不真实的。

类型防护

上面使用的`User’类型的类型保护,最简单的形式如下。请注意,上面解释的与NaN有关的特殊功能并不是其中的一部分,因此这个类型保护并不完全正确。

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

类型保护器总是返回一个布尔值,通常直接用于If操作。

const data: any = await fetch('/user/1');

if (isUser(data)) {
    data.id; //can be safely accessed and is a number
}

为每个类型保护器编写一个单独的函数,特别是对于更复杂的类型,然后在每次类型改变时调整它是非常繁琐的,容易出错,而且没有效率。因此,Deepkit提供了函数`is`,它为任何TypeScript类型自动提供了一个类型保护器。这就自动考虑到了一些特殊的特点,比如上面提到的NaN的问题。函数`is`的作用与`validate`相同,但它不是返回一个错误数组,而是简单地返回一个布尔值。

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
}

经常可以发现的一种模式是,如果验证失败,直接返回一个错误,这样就不会执行后续的代码。这可以在不同的地方使用,而不改变代码的完整流程。

function addUser(data: any): void {
    if (!is<User>(data)) throw new TypeError('No user given');

    //data is guaranteed to be of type User now
}

另外,还可以使用TypeScript类型断言。如果给定的数据不能正确验证为一个类型,函数`assert`会自动抛出一个错误。函数的特殊签名,区别于TypeScript的类型断言,有助于TypeScript自动细化('缩小')传递的变量。

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
}

再次,利用TypeScript为他们提供的好处。类型可以被各种TypeScript函数重用或定制。

错误报告

函数`is`、assert`和`validates`返回一个布尔值作为结果。为了获得验证规则失败的确切信息,可以使用函数`validate。如果所有东西都被成功验证,它将返回一个空数组。如果出现错误,数组包含一个或多个条目,其结构如下:

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,
}

该函数接收作为第一类型参数的任意TypeScript类型和作为第一参数的要验证的数据。

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: ''}]

复杂的类型如接口、类或泛型也可以在这里使用。

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'}); //[]

约束

除了检查类型,其他任意的约束可以被添加到一个类型。这些额外的内容约束的检查是在类型本身被检查后自动完成的。这是在所有验证函数中进行的,如`validate`、is`和`assert。一个限制可以是,例如,一个字符串必须有一定的最小或最大长度。 这些限制通过类型装饰器添加到实际类型中。有一整套可以使用的装饰器。如果需要扩展,可以随意定义和使用自定义装饰器。

type Username = string & MinLength<3>;

任何数量的类型装饰器都可以用`&`添加到实际类型中。结果,这里是`username',然后可以在所有的验证函数中使用,也可以在其他类型中使用。

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

函数`validate’给出了有用的错误信息,这些信息来自于限制。

const errors = validate<Username>('xb');
//[{ code: 'minLength', message: `Min length is 3` }]

例如,这些信息也可以奇妙地自动显示在表单上,并通过`code’翻译。使用对象和数组的现有路径,表单中的字段可以过滤出并显示适当的错误。

validate<User>({id: 1, username: 'ab'});
//{ path: 'username', code: 'minLength', message: `Min length is 3` }

一个经常有用的用例也是用一个正则约束来定义一个电子邮件。一旦定义了类型,就可以在任何地方使用。

export const emailRegexp = /^\[email protected]\S+$/;
type Email = string & Pattern<typeof emailRegexp>

is<Email>('abc'); //false
is<Email>('[email protected]'); //true

可以添加任何数量的约束。

type ID = number & Positive & Maximum<1000>;

is<ID>(-1); //false
is<ID>(123); //true
is<ID>(1001); //true

约束类型

Validate<typeof MyValidator>

使用自定义验证函数进行验证。

	type T = string & Validate<typeof myValidator>

Pattern<typeof MyRegexp>

定义一个正则表达式作为验证模式,更多信息见下一节自定义验证器。通常用于电子邮件验证或更复杂的内容验证。

	const myRegExp = /[a-zA-Z]+/;
	type T = string & Pattern<typeof myRegExp>

Alpha

验证字母字符(a-Z)。

	type T = string & Alpha;

Alphanumeric

验证字母和数字字符。

	type T = string & Alphanumeric;

Ascii

验证ASCII字符。

	type T = string & Ascii;

Decimal<number, Number>

验证代表十进制数字的字符串,如0.1,.3,1.1,1.00003,4.0等。

	type T = string & Decimal<1, 2>;

MultipleOf<number>

验证数字是给定数字的倍数。

	type T = number & MultipleOf<3>;

MinLength<number>, MaxLength<number>

验证数组或字符串的最小/最大长度。

	type T = any[] & MinLength<1>;

	type T = string & MinLength<3> & MaxLength<16>;

Includes<'any'> Excludes<'any'>

验证一个数组项或子字符串被包括/排除

	type T = any[] & Includes<'abc'>;
	type T = string & Excludes<' '>;

Minimum<number>, Maximum<number>

验证一个值是最小或最大的给定数字。与`>=<=`相同。

	type T = number & Minimum<10>;
	type T = number & Minimum<10> & Maximum<1000>;

ExclusiveMinimum<number>, ExclusiveMaximum<number>

与最小/最大值相同,但不包括值本身。

	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;

Email

通过`/^\[email protected]\S+$/`对电子邮件进行简单的regexp验证。

	type T = Email;

Integer

确保数字是正确范围内的整数,所以不需要做 "string & Email"。是自动的 "数字",所以不需要做 "数字和整数"。

	type T = integer;
	type T = uint8;
	type T = uint16;
	type T = uint32;
	type T = int8;
	type T = int16;
	type T = int32;

更多信息见特殊类型:整数/浮点

自定义验证器

如果内置的验证器不够用,可以通过`Validate`装饰器创建和使用自定义验证器函数。

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]

请注意,你的自定义验证器函数是在所有内置类型验证被调用后执行。如果一个验证器失败了,当前类型的所有后续验证器都被省略。每个类型只可能有一次失败。

通用验证器

在验证器函数中,类型对象是可用的,可以使用验证器来获得更多关于类型的信息。也有一种方法可以定义任何必须传递给验证类型的验证器选项,使验证器可配置。有了这些信息及其父级引用,就可以创建强大的通用验证器。

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` }]);