mistermicheels 原作,授权 New Frontend 翻译。
为什么需要额外的类型检查?
TypeScript 只在编译期执行静态类型检查!实际运行的是从 TypeScript 编译的 JavaScript,这些生成的 JavaScript 对类型一无所知。编译期静态类型检查在代码库内部能发挥很大作用,但对不合规范的输入(比如,从 API 处接收的输入)无能为力。
运行时检查的严格性
- 至少需要和编译期检查一样严格,否则就失去了编译期检查提供的保证。
- 如有必要,可以比编译期检查更严格,例如,年龄需要大于等于 0。
运行时类型检查策略
定制代码手动检查
- 灵活
- 可能比较枯燥,容易出错
- 容易和实际代码脱节
使用校验库手动检查
比如使用 joi:
import Joi from "@hapi/joi"
const schema = Joi.object({
firstName: Joi.string().required(),
lastName: Joi.string().required(),
age: Joi.number().integer().min(0).required()
});
- 灵活
- 容易编写
- 容易和实际代码脱节
手动创建 JSON Schema
例如:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"firstName",
"lastName",
"age"
],
"properties": {
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"age": {
"type": "integer",
"minimum": 0
}
}
}
- 使用标准格式,有大量库可以校验。
- JSON 很容易存储和复用。
- 可能会很冗长,手写 JSON Schema 可能会很枯燥。
- 需要确保 Schema 和代码同步更新。
自动创建 JSON Schema
基于 TypeScript 代码生成 JSON Schema
- 比如 typescript-json-schema 这个工具就可以做到这一点(同时支持作为命令行工具使用和通过代码调用)。
- 需要确保 Schema 和代码同步更新。
基于 JSON 输入示例生成
- 没有使用已经在 TypeScript 代码中定义的类型信息。
- 如果提供的 JSON 输入示例和实际输入不一致,可能导致错误。
- 仍然需要确保 Schema 和代码同步更新。
转译
例如使用 ts-runtime。
这种方式会将代码转译成功能上等价但内置运行时类型检查的代码。
比如,下面的代码:
interface Person {
firstName: string;
lastName: string;
age: number;
}
const test: Person = {
firstName: "Foo",
lastName: "Bar",
age: 55
}
会被转译为:
import t from "ts-runtime/lib";
const Person = t.type(
"Person",
t.object(
t.property("firstName", t.string()),
t.property("lastName", t.string()),
t.property("age", t.number())
)
);
const test = t.ref(Person).assert({
firstName: "Foo",
lastName: "Bar",
age: 55
});
这一方式的缺陷是无法控制在何处进行运行时检查(我们只需在输入输出的边界处进行运行时类型检查)。
顺便提一下,这是一个实验性的库,不建议在生产环境使用。
运行时类型派生静态类型
比如使用 io-ts 这个库。
这一方式下,我们定义运行时类型,TypeScript 会根据我们定义的运行时类型推断出静态类型。
运行时类型示例:
import t from "io-ts";
const PersonType = t.type({
firstName: t.string,
lastName: t.string,
age: t.refinement(t.number, n => n >= 0, 'Positive')
})
从中提取相应的静态类型:
interface Person extends t.TypeOf<typeof PersonType> {}
以上类型等价于:
interface Person {
firstName: string;
lastName: string;
age: number;
}
- 类型总是同步的。
- [io-ts] 很强大,比如支持递归类型。
需要将类型定义为 io-ts 运行时类型,这在定义类时不适用:
- 有一种变通的办法是使用 io-ts 定义一个接口,然后让类实现这个接口。然而,这意味着每次给类增加属性的时候都要更新 io-ts 类型。
- 不容易复用接口(比如前后端之间使用同一接口),因为这些接口是 io-ts 类型而不是普通的 TypeScript 类型。
基于装饰器的类校验
比如使用 class-validator 这个库。
- 基于类属性的装饰器。
和 Java 的 JSR-380 Bean Validation 2.0 (比如 Hibernate Validator 就实现了这一标准)很像。
- 此类 Java EE 风格的库还有 typeorm (ORM 库,类似 Java 的 JPA)和 routing-controllers (用于定义 API,类似 Java 的 JAX-RS)。
代码示例:
import { plainToClass } from "class-transformer";
import {
validate, IsString, IsInt, Min
} from "class-validator";
class Person {
@IsString()
firstName: string;
@IsString()
lastName: string;
@IsInt()
@Min(0)
age: number;
}
const input: any = {
firstName: "Foo",
age: -1
};
const inputAsClassInstance = plainToClass(
Person, input as Person
);
validate(inputAsClassInstance).then(errors => {
// 错误处理代码
});
- 类型总是同步的。
- 需要对类进行检查时很有用。
- 可以用来检查接口(定义一个实现接口的类)。
注意:class-validator 用于具体的类实例。在上面的代码中,我们使用它的姊妹库 class-transformer
将普通输入转换为 Person
实例。转换过程本身不进行任何类型检查。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。