TypeScript:验证外部数据
数据验证意味着确保数据具有所需的结构和内容。
使用 TypeScript,当我们收到外部数据时,验证变得相关,例如:
- 从 JSON 文件解析的数据
- 从网络服务接收的数据
在这些情况下,我们希望数据适合我们拥有的静态类型,但我们不能确定。与我们自己创建的数据相比,TypeScript 会不断检查一切是否正确。
这篇博文解释了如何在 TypeScript 中验证外部数据。
JSON 模式
在我们探索 TypeScript 中的数据验证方法之前,我们需要先了解一下JSON 模式,因为有几种方法是基于它的。
JSON 模式背后的思想是用JSON来表达JSON 数据的模式(结构和内容,认为是静态类型)。也就是说,元数据以与数据相同的格式表示。
JSON 模式的用例是:
- 验证 JSON 数据:如果我们有数据的模式定义,我们可以使用工具来检查数据是否正确。数据的一个问题也可以自动修复:我们可以指定可用于添加缺少的属性的默认值。
- 记录 JSON 数据格式:一方面,核心模式定义可以被视为文档。但是 JSON 模式还支持描述、弃用说明、注释、示例等。这些机制称为注解。它们不用于验证,而是用于文档。
- IDE 支持编辑数据:例如,Visual Studio Code 支持 JSON 架构。如果有 JSON 文件的模式,我们将获得几个编辑功能:自动完成、错误突出显示等。值得注意的是,VS Code 对
package.json
文件的支持完全基于 JSON 模式。
一个示例 JSON 模式
这个例子是取自该json-schema.org
网站:
{
"$id": "https://example.com/geographical-location.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Longitude and Latitude Values",
"description": "A geographical coordinate.",
"required": [ "latitude", "longitude" ],
"type": "object",
"properties": {
"latitude": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"longitude": {
"type": "number",
"minimum": -180,
"maximum": 180
}
}
}
以下 JSON 数据在此架构中有效:
{
"latitude": 48.858093,
"longitude": 2.294694
}
TypeScript 中的数据验证方法
本节简要概述了在 TypeScript 中验证数据的各种方法。对于每种方法,我都会列出一个或多个支持该方法的库。Wrt 库,我不打算全面,因为这个领域的事情变化很快。
不使用 JSON 模式的方法
- 方法:调用构建器方法和函数来生成验证函数(在运行时)和静态类型(在编译时)。图书馆包括:
- 方法:调用构建器方法并仅生成验证函数。采用这种方法的图书馆通常专注于使验证尽可能通用:
- 方法:在编译时将 TypeScript 类型编译为验证代码。图书馆:
使用 JSON 模式的方法
- 方法:将 TypeScript 类型转换为 JSON 模式。图书馆:
- 方法:将 JSON 模式转换为 TypeScript 类型。图书馆:
- 方法:通过 JSON 模式验证 JSON 数据。前两种方法也倾向于这样做。npm 包:
选择图书馆
- 方法:将 TypeScript 类型转换为 JSON 模式。图书馆:
- 方法:将 JSON 模式转换为 TypeScript 类型。图书馆:
- 方法:通过 JSON 模式验证 JSON 数据。前两种方法也倾向于这样做。npm 包:
选择图书馆
使用哪种方法和库,取决于我们需要什么:
- 如果我们从 TypeScript 类型开始并希望确保数据(来自配置文件等)适合这些类型,那么支持静态类型的构建器 API 是一个不错的选择。
- 如果我们的起点是 JSON 模式,那么我们应该考虑支持 JSON 模式的库之一。
- 如果我们正在处理更混乱的数据(例如通过表单提交),我们可能需要一种更灵活的方法,其中静态类型的作用较小。
示例:通过库Zod 验证数据
通过 Zod 的构建器 API 定义“模式”
Zod 有一个生成器 API,可以生成类型和验证函数。该 API 的用法如下:
import * as z from 'zod';
const FileEntryInputSchema = z.union([
z.string(),
z.tuple([z.string(), z.string(), z.array(z.string())]),
z.object({
file: z.string(),
author: z.string().optional(),
tags: z.array(z.string()).optional(),
}),
]);
对于较大的模式,将事物分解为多个const
声明是有意义的。
Zod 可以从 生成静态类型FileEntryInputSchema
,但我决定(冗余!)手动维护静态类型FileEntryInput
:
type FileEntryInput =
| string
| [string, string, string[]]
| {file: string, author?: string, tags?: string[]}
;
为什么要裁员?
- 阅读起来更容易。
- 如果我必须这样做,它有助于迁移到不同的验证库或方法。
Zod 生成的类型仍然有用,因为我们可以检查它是否可以分配给FileEntryInput
. 这将警告我们与两者不同步相关的大多数问题。
验证数据
以下函数检查参数是否data
符合FileEntryInputSchema
:
function validateData(data: unknown): FileEntryInput {
return FileEntryInputSchema.parse(data); // may throw an exception
}
validateData(['iceland.txt', 'me', ['vacation', 'family']]); // OK
assert.throws(
() => validateData(['iceland.txt', 'me']));
结果的静态类型FileEntryInputSchema.parse()
是 Zod 派生的FileEntryInputSchema
。通过使FileEntryInput
返回类型validateData()
,我们确保前一种类型可分配给后者。
类型保护
FileEntryInputSchema.check()
是一个类型保护:
function func(data: unknown) {
if (FileEntryInputSchema.check(data)) {
// %inferred-type: string
// | [string, string, string[]]
// | { author?: string | undefined; tags?: string[] | undefined; file: string; }
data;
}
}
定义一个支持FileEntryInput
而不是 Zod 推断的自定义类型保护是有意义的。
function isValidData(data: unknown): data is FileEntryInput {
return FileEntryInputSchema.check(data);
}
从 Zod 模式派生静态类型
参数化类型z.infer<Schema>
可用于从模式派生类型:
// %inferred-type: string
// | [string, string, string[]]
// | { author?: string | undefined; tags?: string[] | undefined; file: string; }
type FileEntryInputStatic = z.infer<typeof FileEntryInputSchema>;
数据的外部与内部表示
在处理外部数据时,区分两种类型通常很有用。
一方面,有描述输入数据的类型。它的结构经过优化,易于创作:
type FileEntryInput =
| string
| [string, string, string[]]
| {file: string, author?: string, tags?: string[]}
;
另一方面,还有程序中使用的类型。其结构经过优化,易于在代码中使用:
type FileEntry = {
file: string,
author: null|string,
tags: string[],
};
在我们使用 Zod 确保输入数据符合 之后FileEntryInput
,我们使用转换函数将数据转换为类型的值FileEntry
。
结论
我的数据验证库用例是确保数据匹配给定的 TypeScript 类型。因此,我宁愿直接将类型编译为验证函数。到目前为止,只有 Babel 宏可以typecheck.macro
做到这一点,它需要 Babel 为我排除了它。我想我也可以使用将 TypeScript 类型编译为具有验证功能的单独模块的工具。但这也有缺点,在可用性方面。
因此,Zod 目前对我来说是一个很好的解决方案,我没有任何遗憾。
对于具有构建器 API 的库,我希望拥有将 TypeScript 类型编译为构建器 API 调用的工具(在线和通过命令行)。这将在两个方面有所帮助:
- 这些工具可用于探索 API 的工作方式。
- 我们可以选择通过工具生成 API 代码。
文章链接:https://www.ooize.com/typescript-validate-external-data.html