知识广场

按学科筛选:计算机科学 / 前端开发 / TypeScript
清除筛选

«计算机科学 / 前端开发 / TypeScript» 分类下共 2 篇帖子

Zod:TS 之外,运行时也要校验数据

## 起因 TypeScript 是编译时类型检查。运行时 API 返回数据 / 用户输入 / localStorage 读出来的东西,TS 不知道: ```ts type User = { id: number; email: string }; const res = await fetch('/api/me'); const user: User = await res.json(); // ⚠️ 类型断言不验运行时 user.email.toLowerCase(); // 万一 API 返回 null 或者 email 是 undefined? ``` 类型 lie → 运行时崩。 `Zod` 是 runtime schema validation + 自动 TS 类型推导。一份 schema 既 是验证 + 又是 type。 ## 基本 ```ts import { z } from 'zod'; const UserSchema = z.object({ id: z.number(), email: z.string().email(), name: z.string().min(1).max(100), age: z.number().int().positive().optional(), }); type User = z.infer<typeof UserSchema>; // { id: number; email: string; name: string; age?: number } const data = await res.json(); const user = UserSchema.parse(data); // throw if invalid // user 类型 = User,且运行时保证符合 ``` 一份 schema 解决: - 运行时验证 - TS 类型(infer) - 错误信息(with 路径) ## 复杂 schema ```ts const PostSchema = z.object({ id: z.string().uuid(), title: z.string(), body: z.string(), status: z.enum(['draft', 'published', 'archived']), tags: z.array(z.string()), author: z.object({ name: z.string(), avatar: z.string().url().optional(), }), createdAt: z.string().datetime(), }); ``` 嵌套 object + array + enum + format check(uuid / url / datetime)。 ## transform ```ts const DateSchema = z.string().datetime().transform((s) => new Date(s)); // input: string // output: Date const result = DateSchema.parse('2025-03-14T10:00:00Z'); // result: Date instance ``` parse 时把 string 自动转 Date。 input / output type 不同(z.infer 拿 output)。 ## refine (自定义校验) ```ts const passwordSchema = z .string() .min(8) .refine((s) => /[A-Z]/.test(s), { message: 'Need uppercase' }) .refine((s) => /\d/.test(s), { message: 'Need digit' }); ``` ## 错误处理 ```ts const result = UserSchema.safeParse(data); if (!result.success) { console.log(result.error.format()); // { // email: { _errors: ['Invalid email'] }, // age: { _errors: ['Expected positive number'] } // } return; } console.log(result.data); // 通过的 typed data ``` `safeParse` 不 throw,返回 result object。 form 验证场景常用。 ## 跟 React Hook Form 集成 ```tsx import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; const FormSchema = z.object({ email: z.string().email(), password: z.string().min(8), }); function LoginForm() { const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(FormSchema), }); return ( <form onSubmit={handleSubmit((data) => console.log(data))}> <input {...register('email')} /> {errors.email && <p>{errors.email.message}</p>} <input {...register('password')} type="password" /> {errors.password && <p>{errors.password.message}</p>} <button>Login</button> </form> ); } ``` form 校验自动用 Zod schema → message 自动显。 ## API 验证 (tRPC / Hono) ```ts // tRPC procedure import { z } from 'zod'; const userRouter = t.router({ create: t.procedure .input(z.object({ email: z.string().email(), name: z.string().min(1), })) .mutation(({ input }) => { // input 类型 + 运行时都符合 return db.user.create({ data: input }); }), }); ``` ```ts // Hono app.post('/users', zValidator('json', UserSchema), (c) => { const user = c.req.valid('json'); // typed return c.json(user); }); ``` backend 入口验证 input → 后续 code 类型 + runtime 一致。 ## 与 yup / joi 对比 | | Zod | yup | joi | |---|---|---|---| | TS 类型推导 | ✅ first-class | 弱 | 弱 | | bundle | 中(14 KB) | 中 | 大 | | API | builder | builder | builder | | 生态 | 大(现代) | 中 | 后端流行 | | 性能 | 中 | 中 | 慢 | TS 项目 → Zod。 JS 老项目 → yup。 Node API server 不要 TS → joi。 ## 与 valibot / arktype 对比 新一代竞争: - **valibot**:tree-shakable,bundle 极小(500B-2KB) - **arktype**:TS-as-schema(直接写 TS 类型当 schema),运行时极快 ```ts // valibot import { object, string, number } from 'valibot'; const UserSchema = object({ email: string([email()]), age: number([integer()]), }); ``` ```ts // arktype import { type } from 'arktype'; const UserSchema = type({ email: 'email', 'age?': 'number.integer>0', }); ``` valibot 适合极致 bundle size(移动 web)。 arktype 适合纯 TS 语法控。 Zod 仍最普及 + 生态最大。 ## ecosystem zod 生态丰富: - `@hookform/resolvers/zod` - `zod-to-openapi`:自动 OpenAPI 3 spec - `zod-to-ts`:生成 TS file - `prisma-zod-generator`:从 Prisma schema 生成 Zod - `zod-form-data`:multipart form 验证 ## 性能 百万次 parse benchmark: | | ops/sec | |---|---| | Zod | 350k | | valibot | 800k | | arktype | 1500k | | yup | 100k | Zod 不是最快但够用。极致性能场景才换。 ## 真实 case:API 返回防御 API 三方 / 老接口经常返回奇怪: ```ts // API 文档说返回 number,实际偶尔 null const result = await fetch('/old/api').then(r => r.json()); // 防御 const ResultSchema = z.object({ count: z.number().nullable().transform((n) => n ?? 0), items: z.array(ItemSchema), }); const safe = ResultSchema.parse(result); // safe.count 永远 number(null 转 0) ``` 避免下游 `result.count + 5` 报 NaN。 ## 复杂 union ```ts const EventSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('click'), x: z.number(), y: z.number() }), z.object({ type: z.literal('input'), value: z.string() }), z.object({ type: z.literal('submit'), formId: z.string() }), ]); const event = EventSchema.parse(data); if (event.type === 'click') { event.x; // typed } ``` `discriminatedUnion` 比 plain union 快 + type narrowing 更准。 ## env 变量验证 ```ts const envSchema = z.object({ DATABASE_URL: z.string().url(), PORT: z.coerce.number().int().positive(), NODE_ENV: z.enum(['development', 'production', 'test']), }); export const env = envSchema.parse(process.env); ``` 启动时验证 env,缺 / 错立刻报。 之后 code 用 `env.PORT` 类型 number 不是 string。 ## 踩过的坑 1. **z.string().uuid() too strict**:传统 v1 UUID 不过。`.uuid()` 是 严格 v4 格式。 2. **.parse vs .safeParse**:parse throw + safeParse 返 result。 form / API boundary 用 safeParse 控制响应;trust source 用 parse 简洁。 3. **infer 大 schema 慢**:深嵌套 union 等 TS 编译慢。break into smaller schema。 4. **transform 后 input type 隐**:`z.infer<typeof schema>` 拿 output; `z.input<typeof schema>` 拿 input。文档 / API 注意区分。 5. **生产 bundle 大**:Zod 14 KB gzip 在 mobile critical 可能多。 考虑 valibot 替换关键路径。

TypeScript strict 模式:从 JS 项目逐步迁移的经验

## 起因 老项目 JS(或松散 TS)想拧紧 strict mode: - `noImplicitAny` - `strictNullChecks` - `strictFunctionTypes` - `noImplicitThis` - `strictPropertyInitialization` - `alwaysStrict` 一次性全开 → 几千 type error → 团队崩。 渐进 rollout 是正解。 ## strict 全开 ```json { "compilerOptions": { "strict": true, // 等价于全开上面 6 个 } } ``` 新项目直接 strict。 ## 老项目分步骤 ```json { "compilerOptions": { "strict": false, "noImplicitAny": true, "strictNullChecks": false, ... } } ``` 一个一个加,每个加完跑全 ts check + 修。 推荐顺序: 1. `noImplicitAny`(多数 type 自动推或 explicit) 2. `strictNullChecks`(最大改动,但最值) 3. `strictFunctionTypes` 4. 其它 ## strictNullChecks 影响 ```ts // 关 strictNullChecks function getName(user) { // user: any return user.name; // 没事 } getName(null); // 没报,但运行时崩 // 开 function getName(user: User | null) { return user.name; // ❌ Object is possibly 'null' } function getNameSafe(user: User | null) { return user?.name; // ✅ } ``` 每函数 explicit handle null。 最大价值:编译期 catch null pointer 问题。 ## 渐进:per-file strict `@ts-strict` 风格 comment / 工具: ```ts // @ts-check // @ts-strict ``` 或者用 `ts-strict-plugin`、单 file `// @ts-expect-error` 标技术债。 ## TypeScript per-file 更激进:拆 tsconfig 多个 project: ```json // tsconfig.strict.json { "extends": "./tsconfig.json", "compilerOptions": { "strict": true }, "include": ["src/strict/**"] } ``` `src/strict/` 下严格,其余宽。 新代码进 strict,老代码慢慢迁。 ## 工具帮迁移 - `ts-migrate`(Airbnb 出):批量 JS → TS + 加 `// @ts-expect-error` - `typescript-strict-plugin`:渐进 strict per-file ## 渐进示例 ``` Week 1: 装 TS, allowJs: true, 跑 baseline Week 2: noImplicitAny on, 修 (100 error) Week 3: 把 critical paths 改 strict (per-file) Week 4: strictNullChecks on, 修 (500 error,多数小) Month 2: 全 strict ``` ## 常见迁移 pattern ```ts // 之前 function process(data) { if (data.user.address.city) { ... } } // 改 1: 类型 interface User { address?: Address } interface Address { city?: string } function process(data: { user: User }) { if (data.user.address?.city) { ... } // optional chaining } // 改 2: 早返 function process(data: { user: User }) { const city = data.user.address?.city; if (!city) return; // city 类型 narrowed string } ``` optional chaining + early return 是最常用 strict-friendly pattern。 ## 第三方 lib 没 type ```ts // 装 @types npm install -D @types/lodash // 没 @types 的 declare module 'weird-lib' { export function doStuff(x: any): any; } ``` 最差情况 cast: ```ts const lib = require('weird-lib') as any; ``` 但 `as any` 会传染 → 限定到边界。 ## any vs unknown ```ts function parse(input: any) { // 危险 return input.value; // 不报,但运行时崩 } function parse(input: unknown) { // 强制处理 if (typeof input === 'object' && input && 'value' in input) { return input.value; // narrowed } } ``` `unknown` 是 type-safe 版 `any`。强制 narrow 后用。 新代码用 unknown 替代 any。 ## strictNullChecks 后的常见错 ```ts // 1. array access const arr: string[] = []; const first: string = arr[0]; // ❌ undefined if empty // noUncheckedIndexedAccess: true const first: string | undefined = arr[0]; // 2. delete const obj: { x?: number } = { x: 1 }; delete obj.x; // 必须先 optional // 3. JSON.parse const data: unknown = JSON.parse(s); // 不能直接 data.x → Zod / type guard ``` ## CI gate ```json // package.json "scripts": { "typecheck": "tsc --noEmit", "typecheck:strict": "tsc --project tsconfig.strict.json --noEmit" } ``` ```yaml # CI - run: pnpm typecheck - run: pnpm typecheck:strict # 只检查 strict files ``` PR 改动 strict file → 必须过 strict check。 慢慢"扩 strict 边界"。 ## 真实迁移 某老项目 (50k LOC JS): - 全开 strict 一次:3000 error,回归测试一周 - 改渐进:4 个月 - 月 1:装 TS + allowJs,0 改 → 跑通 - 月 2:noImplicitAny + 主要路径手加 type → 50% 文件 typed - 月 3:strictNullChecks,整修 → 1500 changes - 月 4:全 strict + tslint → strict - 期间也持续开发新 feature 迁完收益: - 生产 NullPointer error 降 80% - IDE auto-complete 准 - 重构信心大增 - 新人 onboarding 快(看类型就懂 API) ## skipLibCheck ```json { "skipLibCheck": true // 第三方 .d.ts 不 check } ``` 大型项目几乎必开。第三方 type 偶有小问题不卡你。 ## 不要追求 100% `any` 不是恶魔。 边界(unknown API / migration code / quick prototype)用 `any` 标 `// FIXME: type later`。 内部核心 type-tight 90% 已经收益大头。 ## 与 jsdoc TS 对比 ```js // JS file + JSDoc /** * @param {string} name * @returns {string} */ function greet(name) { return `Hi ${name}`; } ``` `// @ts-check` 让 TS check JSDoc 类型。 适合:不想转 .ts 但想要 type-check(如 lib / 小项目)。 中大型 → 直接 .ts。 ## 踩过的坑 1. **`Object.keys` 返回 string[]**:`Object.keys({a:1, b:2}).forEach((k) => obj[k])` 报 string can't index Foo。`(Object.keys(obj) as (keyof Foo)[])` cast。 2. **DOM null**:`document.querySelector('.x')` 返 Element | null。 `!` non-null assert 慎用。 3. **callback this**:strict 模式下 method as callback 失 `this`。 bind / arrow。 4. **enum 类型**:`enum X { A, B }` 编译有运行时 object。 `const X = { A: 'a', B: 'b' } as const; type X = (typeof X)[keyof typeof X]` 更轻。 5. **复杂 generic**:递归 / mapped type 编译慢。简化或拆。