# ADR-004: 全 TypeScript strict + zod 运行时校验,全程禁止 any ## Status: Accepted (2026-08-26) ## Background 用户硬性要求:全 TypeScript,**全程禁止 any**(评审铁律)。同时本项目数据来自**本地 JSON 文件**——文件是可变的外部输入(可能被手工编辑、被旧版本程序写过、Docker 挂载来源不可控),若不运行时校验,读入的数据无法保证符合领域类型。 ## Decision 1. **tsconfig 全开严格选项**(`strict` 全家桶): `strict / noImplicitAny / strictNullChecks / strictFunctionTypes / exactOptionalPropertyTypes / useUnknownInCatchVariables / noUncheckedIndexedAccess / noImplicitReturns / noUnusedLocals...` 2. **类型定义集中 + 单一契约源**:领域类型由 `packages/shared/src/schemas/*.schema.ts` 的 zod schema 经 `z.infer` 产出(不手写 interface),前后端共用;统一响应 `ApiResponse = ApiSuccess | ApiFailure`(泛型,0 any)。 3. **错误用联合类型**:`AppError` 带错误码联合 + message;`catch` 变量为 `unknown`,用类型守卫窄化,**不 any 强转**。 4. **zod 运行时校验获得类型收窄**:数据来自 JSON 文件,用 `zod` schema `parse(raw)`(返回类型即 T,天然收窄)。这是「从文件读数据 + 无 any」的最优实践。`z.infer` 产出具名类型。 5. **OpenAPI 契约同源**:openapi.yaml 由 `zod-openapi` 从同一批 zod schema 自动反推生成,前端据此生成 TS 类型,无需另维护一份手写契约。 ### zod 示例(纯 TS,无 any) ```ts import { z } from 'zod'; const ModuleKeySchema = z.enum(['xingce-shuli', 'xingce-panduan', /** ... */]); export type ModuleKey = z.infer; const QuestionSchema = z.object({ id: z.string().uuid(), subject: z.enum(['xingce', 'shenlun']), module: ModuleKeySchema, type: z.enum(['single', 'multiple', 'judge', 'blank', 'essay']), stem: z.string().min(1), options: z.array(z.string()).optional(), answer: z.union([z.string(), z.array(z.string())]), // ... 全部显式,无 any }).openapi('Question'); export type Question = z.infer; ``` ## Consequences - 正面:`any` 从类型系统层面被禁止;zod 让「不可信的 JSON 输入」在数据层就被收窄为强类型实体,后续代码零断言;前后端共享同一 schema,契约一致;领域类型、OpenAPI 契约、请求校验三者同源于 zod schema,改一处全链跟随,杜绝漂移。 - 负面:引入 zod + zod-openapi 依赖;strict 选项与单一契约源会放大生成代码的报错面(但这是隔离性投资,越快暴露越好,避免把 `unknown` 漏到业务层)。 - 权衡:以 zod 作为**唯一**类型与契约来源,不另维护手写 interface 或手写 openapi.yaml,消除「type 与 schema 两处对不上」的隐患。 ## Related ADRs - ADR-003(JSON 数据层——zod 是其关键保障) - ADR-006(垂直切片 + 单一契约源——zod schema 驱动类型与契约) - ADR-001 / ADR-002(前后端均全 TS)