3.0 KiB
3.0 KiB
ADR-004: 全 TypeScript strict + zod 运行时校验,全程禁止 any
Status: Accepted (2026-08-26)
Background
用户硬性要求:全 TypeScript,全程禁止 any(评审铁律)。同时本项目数据来自本地 JSON 文件——文件是可变的外部输入(可能被手工编辑、被旧版本程序写过、Docker 挂载来源不可控),若不运行时校验,读入的数据无法保证符合领域类型。
Decision
- tsconfig 全开严格选项(
strict全家桶):strict / noImplicitAny / strictNullChecks / strictFunctionTypes / exactOptionalPropertyTypes / useUnknownInCatchVariables / noUncheckedIndexedAccess / noImplicitReturns / noUnusedLocals... - 类型定义集中 + 单一契约源:领域类型由
packages/shared/src/schemas/*.schema.ts的 zod schema 经z.infer产出(不手写 interface),前后端共用;统一响应ApiResponse<T> = ApiSuccess<T> | ApiFailure(泛型,0 any)。 - 错误用联合类型:
AppError带错误码联合 + message;catch变量为unknown,用类型守卫窄化,不 any 强转。 - zod 运行时校验获得类型收窄:数据来自 JSON 文件,用
zodschemaparse(raw)(返回类型即 T,天然收窄)。这是「从文件读数据 + 无 any」的最优实践。z.infer产出具名类型。 - OpenAPI 契约同源:openapi.yaml 由
zod-openapi从同一批 zod schema 自动反推生成,前端据此生成 TS 类型,无需另维护一份手写契约。
zod 示例(纯 TS,无 any)
import { z } from 'zod';
const ModuleKeySchema = z.enum(['xingce-shuli', 'xingce-panduan', /** ... */]);
export type ModuleKey = z.infer<typeof ModuleKeySchema>;
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<typeof QuestionSchema>;
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)