验证
验证是系统性地核验数据准确性和完整性的过程。这不仅包括检查数据类型是否与期望类型一致,还包括是否满足任何额外的预定义约束。
当处理来自不确定或不受信任来源的数据时,验证尤为重要。“不确定”的来源指的是其数据的类型或内容不可预测,运行时可能取任意值。典型例子包括用户输入、来自 HTTP 请求的数据(如查询参数或请求体)、CLI 参数,或程序读取的文件。此类数据天然存在风险,因为错误的类型或数值可能导致程序失败,甚至引入安全漏洞。
例如,如果变量预期存储一个数字,那么验证其确实为数值至关重要。类型不匹配可能导致意外崩溃或安全漏洞。
在设计 HTTP 路由控制器时,必须优先验证所有用户输入,无论是通过查询参数、请求体,还是其他方式。特别是在使用 TypeScript 的环境中,务必避免类型断言(type cast)。这些断言可能具有误导性,并引入根本性的安全风险。
app.post('/user', function(request) { const limit = request.body.limit as number; });
编码中常见的错误是依赖在运行时并不提供安全性的类型断言。例如,如果你将变量断言为 number,但用户却输入了字符串,程序就会被误导,按“该字符串是数字”来运行。这类疏忽可能导致系统崩溃或引发严重的安全威胁。为降低这些风险,开发者可以利用验证器(validator)和类型保护(type guard)。此外,序列化器(serializer)也可用于转换变量,如将 limit 转为数字。更多见“序列化”章节。
验证不是可选项;它是健壮软件设计的组成部分。宁可验证过度,也不要因验证不足而后悔。Deepkit 深知其重要性,提供了丰富的验证工具,而且其高性能设计确保对执行时间的影响最小。作为指导原则,请进行全面验证来保护你的应用,即使有时看起来有点“啰嗦”。
Deepkit 的许多组件,包括 HTTP 路由器、RPC 抽象,甚至数据库抽象,都内置了验证系统。这些机制会自动触发,通常无需手动干预。
要全面了解自动验证何时以及如何发生,请参阅特定章节(CLI、HTTP、RPC、ORM)。 熟悉必要的约束与数据类型。合理定义参数可以释放 Deepkit 的自动化验证潜力,减少手工工作,确保代码更简洁、更安全。
用法
验证器的基本功能是按类型检查值。例如,某个值是否为字符串。这里不关心字符串的内容,仅关心其类型。TypeScript 中有许多类型:string、number、boolean、bigint、对象、类、interface、泛型、映射类型等。得益于 TypeScript 强大的类型系统,可用的类型种类非常丰富。
在 JavaScript 中,原始类型可以通过 typeof
运算符解析。对于更复杂的类型,如 interface、映射类型或泛型的 set/map,这就不再容易,像 @deepkit/type
这样的验证库就变得必不可少。Deepkit 是唯一能够直接验证所有 TypeScript 类型而无需任何变通方法的解决方案。
在 Deepkit 中,可以使用 validate
、is
或 assert
函数进行类型验证。
is
是所谓的类型保护(type guard),assert
是类型断言(type assertion)。两者会在下一节解释。
validate
函数返回一个包含发现错误的数组,成功时返回空数组。该数组中的每一项描述了确切的错误代码、错误信息,以及在验证更复杂类型(如对象或数组)时的路径。
这三个函数的使用方式大致相同。将类型作为第一个类型参数指定或引用,并将数据作为第一个函数参数传入。
import { validate, is, assert } from '@deepkit/type'; const errors = validate<string>('abc'); //[] const errors = validate<string>(123); //[{code: 'type', message: '不是字符串'}] if (is<string>(value)) { // value 现在被保证为字符串 } function doSomething(value: any) { assert<string>(value); //在数据无效时抛出 // value 现在被保证为字符串 }
如果你使用更复杂的类型,如类或接口,返回的数组也可能包含多条目。
import { validate } from '@deepkit/type'; interface User { id: number; username: string; } validate<User>({id: 1, username: 'Joe'}); //[] validate<User>(undefined); //[{code: 'type', message: '不是对象'}] validate<User>({}); //[ // {path: 'id', code: 'type', message: '不是数字'}], // {path: 'username', code: 'type', message: '不是字符串'}], //]
验证器也支持深度递归类型。路径将用点号分隔。
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: '不是数字'}], // {path: 'supervisor.username', code: 'type', message: '不是字符串'}], //]
充分利用 TypeScript 带来的优势。例如,更复杂的类型如 User
可以在多个地方复用,而无需一次次重复声明。比如如果需要在不包含 id
的情况下验证 User
,可以使用 TypeScript 的工具类型快速高效地创建派生子类型。这非常符合 DRY(不要重复自己)的精神。
type UserWithoutId = Omit<User, 'id'>; validate<UserWithoutId>({username: 'Joe'}); //有效!
Deepkit 是唯一能够在运行时以这种方式访问 TypeScript 类型的主流框架。如果想在前端和后端中共享类型,可以将类型提取到单独文件,从而在任意位置导入。利用这一点有助于保持代码高效且整洁。
类型断言不安全
TypeScript 中的类型断言(与类型保护相对)并非运行时构造,它只在类型系统中生效。用它给未知数据指定类型并不安全。
const data: any = ...; const username = data.username as string; if (username.startsWith('@')) { //可能会崩溃 }
as string
这段代码并不安全。变量 data
可能是任何值,例如 {username: 123}
,甚至 {}
,导致 username
并不是字符串,而是完全不同的东西,因此 username.startsWith('@')
会报错,在基础情况下导致程序崩溃,在最糟糕的情况下造成安全漏洞。
要在运行时保证 data
拥有类型为字符串的 username
属性,必须使用类型保护。
类型保护是向 TypeScript 提示传入数据在运行时被保证为何种类型的函数。借助这些信息,TypeScript 会在代码执行过程中“收窄”类型。例如,可以将 any
安全地缩窄为字符串或其他类型。因此,如果存在类型未知的数据(any
或 unknown
),类型保护可以基于数据本身更精确地缩窄它。然而,类型保护的安全性取决于其实现。如果实现有误,后果可能非常严重,因为基本假设会突然变得不成立。
类型保护(Type-Guard)
对上文使用的 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; //可以安全访问,且为数字 }
为每个类型手写一个类型保护函数,尤其是针对更复杂的类型,并在类型变化时反复修改,既繁琐又易出错且低效。因此,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 现在被保证为 User 类型 }
一种常见的模式是在验证失败时直接返回错误,以阻止后续代码执行。这可在多处使用,而无需改变整体代码流程。
function addUser(data: any): void { if (!is<User>(data)) throw new TypeError('No user given'); //data 现在被保证为 User 类型 }
或者,可以使用 TypeScript 的类型断言。assert
函数会在给定数据未能正确通过类型验证时自动抛出错误。该函数的特殊签名(区别于普通函数)帮助 TypeScript 自动收窄传入变量。
import { assert } from '@deepkit/type'; function addUser(data: any): void { assert<User>(data); //数据无效时抛出 //data 现在被保证为 User 类型 }
在这里,同样要发挥 TypeScript 的优势。可以通过各种 TypeScript 功能复用或定制类型。
错误报告
is
、assert
和 validates
函数的结果是布尔值。若要获取验证失败规则的详细信息,可使用 validate
函数。若全部验证成功,它返回空数组;出错时,数组包含一条或多条结构如下的条目:
interface ValidationErrorItem { /** * 指向属性的路径。对于深层路径,用点号分隔。 */ path: string; /** * 小写的错误代码,可用于标识该错误并进行翻译。 */ code: string, /** * 错误的自由文本描述。 */ message: string, }
该函数将任意 TypeScript 类型作为第一个类型参数,待验证的数据作为第一个实参。
import { validate } from '@deepkit/type'; validate<string>('Hello'); //[] validate<string>(123); //[{code: 'type', message: '不是字符串', path: ''}] validate<number>(123); //[] validate<number>('Hello'); //[{code: 'type', message: '不是数字', path: ''}]
也可用于复杂类型,如 interface、class 或泛型。
import { validate } from '@deepkit/type'; interface User { id: number; username: string; } validate<User>(undefined); //[{code: 'type', message: '不是对象', path: ''}] validate<User>({}); //[{code: 'type', message: '不是数字', path: 'id'}] validate<User>({id: 1}); //[{code: 'type', message: '不是字符串', path: 'username'}] validate<User>({id: 1, username: 'Peter'}); //[]
约束
除了类型检查,还可以为类型添加其他任意约束。这些附加的内容约束会在类型本身验证通过后自动进行。这适用于所有验证函数,如 validate
、is
和 assert
。
一个约束例如可以是字符串必须有一定的最小/最大长度。这些约束通过类型注解添加到实际类型上。可用的注解种类很多。如有更复杂需求,也可以按需定义并使用自定义注解。
import { MinLength } from '@deepkit/type'; type Username = string & MinLength<3>;
使用 &
可以将任意数量的类型注解添加到实际类型上。结果类型(此处为 username
)随后可用于所有验证函数以及其他类型中。
import { is } from '@deepkit/type'; is<Username>('ab'); //false,因为最小长度为 3 is<Username>('Joe'); //true interface User { id: number; username: Username; } is<User>({id: 1, username: 'ab'}); //false,因为最小长度为 3 is<User>({id: 1, username: 'Joe'}); //true
validate
函数会给出来自约束的有用错误信息。
import { validate } from '@deepkit/type'; const errors = validate<Username>('xb'); //[{ code: 'minLength', message: `最小长度为 3` }]
这些信息可以很方便地在表单中自动呈现,并可通过 code
进行翻译。借助对象和数组的路径信息,表单中的字段可以筛选并显示相应错误。
validate<User>({id: 1, username: 'ab'}); //{ path: 'username', code: 'minLength', message: `最小长度为 3` }
一个常见且有用的用例是使用正则表达式约束来定义 Email。一旦定义了该类型,就可以在任何地方复用。
export const emailRegexp = /^\S+@\S+$/; type Email = string & Pattern<typeof emailRegexp> is<Email>('abc'); //false is<Email>('joe@example.com'); //true
可以添加任意数量的约束。
type ID = number & Positive & Maximum<1000>; is<ID>(-1); //false is<ID>(123); //true is<ID>(1001); //true
约束类型
Validate
使用自定义验证函数进行验证。更多信息见下一节“自定义验证器”。
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
定义一个正则表达式作为验证模式。通常用于 Email 验证或更复杂的内容验证。
import { Pattern } from '@deepkit/type'; const myRegExp = /[a-zA-Z]+/; type T = string & Pattern<typeof myRegExp>
Alpha
仅包含字母字符(a-Z)的验证。
import { Alpha } from '@deepkit/type'; type T = string & Alpha;
Alphanumeric
字母与数字字符的验证。
import { Alphanumeric } from '@deepkit/type'; type T = string & Alphanumeric;
Ascii
ASCII 字符的验证。
import { Ascii } from '@deepkit/type'; type T = string & Ascii;
Decimal<number, number>
验证字符串表示一个十进制数,如 0.1、.3、1.1、1.00003、4.0 等。
import { Decimal } from '@deepkit/type'; type T = string & Decimal<1, 2>;
MultipleOf
验证数字是给定数字的倍数。
import { MultipleOf } from '@deepkit/type'; type T = number & MultipleOf<3>;
MinLength
为数组或字符串验证最小/最大长度。
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'>
验证数组项或子字符串被包含/排除。
import { Includes, Excludes } from '@deepkit/type'; type T = any[] & Includes<'abc'>; type T = string & Excludes<' '>;
Minimum
验证数值不小于/不大于给定数字。对应 >=
和 <=
。
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
与 Minimum/Maximum 类似,但不包含边界值本身。对应 >
和 <
。
import { ExclusiveMinimum, ExclusiveMaximum } from '@deepkit/type'; type T = number & ExclusiveMinimum<10>; type T = number & ExclusiveMinimum<10> & ExclusiveMaximum<1000>;
Positive, Negative, PositiveNoZero, NegativeNoZero
验证数值为正或为负。
import { Positive, Negative } from '@deepkit/type'; type T = number & Positive; type T = number & Negative;
BeforeNow, AfterNow
将日期值与当前时间(new Date)比较的验证。
import { BeforeNow, AfterNow } from '@deepkit/type'; type T = Date & BeforeNow; type T = Date & AfterNow;
通过 /^\S+@\S+$/
进行简单的邮箱正则验证。它自动为 string
,因此无需写成 string & Email
。
import { Email } from '@deepkit/type'; type T = Email;
integer
确保数字是在正确范围内的整数。它自动为 number
,因此无需写成 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;
更多信息见“特殊类型:整数/浮点数”。
自定义验证器
如果内置验证器不够用,可以通过 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` }]);