TypeScript 入门教程:03 — TypeBox 入门:一份定义,双重用途 1. 你以前怎么惨你写了一个用户注册 API。用interface定义了请求体interfaceRegisterBody{username:string;password:string;age:number;}TypeScript 在编译时帮你检查。同事写前端代码时传了{ username: 123, password: true }——IDE 直接红了好拦住了。然后 API 上线了。有人用 curl 发了一个请求curl-XPOST /api/register-d{username: hacker, password: 123, age: thirty}password是数字age是字符串。interface在编译成 JS 之后消失了。express收到的就是原始 JSON 对象没有任何验证。你的代码里写body.age.toFixed(2)——炸了。数据库里塞进了password: 123一个数字而不是哈希——安全漏洞。interface的问题它是编译时的幻影。编译成 JS 之后它不存在。运行时的数据不会经过它。2. 先看一眼烂代码// 方式一interface 手工 if 验证 —— 写两遍interfaceUser{name:string;age:number;}functionvalidateUser(raw:unknown):User|null{if(typeofraw!object||rawnull)returnnull;constobjrawasRecordstring,unknown;if(typeofobj.name!string)returnnull;if(typeofobj.age!number)returnnull;returnobjasUser;}// 问题interface 定义了一遍字段if 验证又定义了一遍。// 加一个字段改两个地方。漏改一处 线上 bug。// 方式二不管了直接 as 断言 —— 鸵鸟算法functionhandleRequest(body:unknown){constuserbodyasUser;console.log(user.name.toUpperCase());// 万一是 null/undefined ~炸}// 问题as 只是骗了 TS 编译器。运行时 body 还是未知的该炸还是炸。两种方式都烂。第一种是重复劳动——字段多了根本维护不了。第二种是把头埋进沙子里——假装数据是干净的直到线上炸了才抬头。3. 用新语法修好它TypeBox 的核心理念一个 schema 定义 编译时类型 运行时验证器。你只写一次两边都有。3.1 描述对象Type.Objectimport{Type,typeStatic}fromsinclair/typebox;// 写一次 schemaconstUserSchemaType.Object({name:Type.String(),age:Type.Number(),email:Type.String(),});// Static 从 schema 推导出类型——等价于你手写 interfacetypeUserStatictypeofUserSchema;// type User { name: string; age: number; email: string }类比TypeBox 的 schema 是一张智能表格。你定义了三个格子要填什么名字填字、年龄填数字。这张表格有两份副本——一份给 TS 编译器看编译时检查代码一份留在内存里运行时检查 HTTP 请求。你只需要定义一次。3.2 运行时验证Value.Checkimport{Value}fromsinclair/typebox/value;functionhandleRequest(raw:unknown):User{if(Value.Check(UserSchema,raw)){// 通过了——raw 就是合法的 Userreturnraw;// TS 在这里自动收窄 raw 类型为 User}thrownewError(无效的用户数据);}Value.Check(schema, data)返回boolean。如果返回trueTS 把data的类型收窄为 schema 对应的类型。你不需要手动写as User而且那是不安全的。Value.Errors(schema, data)返回详细的错误列表——哪个字段错了、错在哪里consterrors[...Value.Errors(UserSchema,{name:123,age:old})];// [// { path: /name, message: Expected string },// { path: /age, message: Expected number },// { path: /email, message: Expected required property },// ]3.3 联合与字面量Type.Union、Type.LiteralTypeBox 的联合和字面量既参与编译时类型推导也在运行时做验证。constStatusSchemaType.Union([Type.Literal(active),Type.Literal(inactive),Type.Literal(banned),]);// Static 推导type Status active | inactive | bannedtypeStatusStatictypeofStatusSchema;Value.Check(StatusSchema,active);// trueValue.Check(StatusSchema,deleted);// false —— 运行时拦住对象里可以嵌套联合constPaymentSchemaType.Union([Type.Object({kind:Type.Literal(alipay),account:Type.String()}),Type.Object({kind:Type.Literal(wechat),openId:Type.String()}),]);// type Payment { kind: alipay; account: string }// | { kind: wechat; openId: string }3.4 可选字段Type.OptionalconstProfileSchemaType.Object({name:Type.String(),bio:Type.Optional(Type.String()),// 可以不填});// type Profile { name: string; bio?: string }和 TS 的?一样——编译时 TS 知道bio可能是undefined运行时 Value.Check 不要求bio必须存在。3.5 一份定义两处使用这是 TypeBox 最大的价值┌─────────────────────┐ │ const Schema │ │ Type.Object({...}) │ ← 你只写这一次 └────────┬────────────┘ │ ┌───────────┴───────────┐ ▼ ▼ ┌──────────┐ ┌──────────────┐ │ 编译时 │ │ 运行时 │ │ TS 类型 │ │ Value.Check() │ │ 检查代码 │ │ 验证 HTTP 请求 │ └──────────┘ └──────────────┘不需要维护两个并行的定义。加一个字段改 schema 一处。编译时类型自动更新运行时验证自动更新。没有忘了同步这回事。4. 什么时候不该用内部函数之间的数据传递函数 A 调用函数 BB 的参数类型用interface就够了。不需要运行时验证——你信任自己的代码。性能敏感的热路径Value.Check会遍历整个对象做类型检查。如果每秒百万次调用换更轻量的方案。简单的字面量联合如left | right直接用 TS 的字面量类型。TypeBox 的字面量真正发挥作用是在运行时验证外部输入时。5. 小结语法一句话示例Type.Object({...})定义对象的形状Type.Object({ name: Type.String() })Type.String()字符串字段Type.String()Type.Number()数字字段可加 minimum/maximumType.Number({ minimum: 0 })Type.Boolean()布尔字段Type.Boolean()Type.Optional(...)可选字段Type.Optional(Type.String())Type.Union([...])运行时联合类型Type.Union([Type.String(), Type.Number()])Type.Literal(v)精确值约束Type.Literal(dark)Statictypeof S从 schema 推导 TS 类型type T Statictypeof SValue.Check(S, d)运行时验证数据if (Value.Check(S, raw)) { ... }