5.8 KiB
5.8 KiB
类型推断问题修复报告
问题描述
在 /home/ubuntu/kevisual/query/test/examples.ts 中,使用 as const 定义的 JSON Schema 对象,其 required 字段没有被正确识别。具体表现为:
- Schema 定义:
required: ["domain"]- 表示只有domain是必需字段 - 实际推断:所有字段(id, domain, appId, status, data)都被推断为必需字段
- 期望行为:只有
domain是必需的,其他字段应该是可选的
根本原因
问题出在 src/query-api.ts 的 InferFromJSONSchema 类型定义中,条件类型的匹配顺序不当。
原始代码(有问题的版本)
type InferFromJSONSchema<T> =
// ... 其他类型 ...
// 对象类型 - 只有 additionalProperties(纯动态对象)
T extends { type: "object"; additionalProperties: infer A }
? (A extends {} ? Record<string, any> : never)
: // 对象类型 - 带 required 字段(必需字段 + 可选字段)
T extends { type: "object"; properties: infer P; required: infer R extends readonly string[] }
? { /* ... */ }
: // ...
问题分析
当 JSON Schema 同时包含 additionalProperties: false 和 required: ["domain"] 时:
- TypeScript 首先尝试匹配第一个条件:
T extends { type: "object"; additionalProperties: infer A } - 匹配成功!因为 Schema 确实有
additionalProperties: false - 此时
A = false,而false extends {}为false - 返回
never,导致类型推断失败或行为异常
关键点:在 JSON Schema 规范中,additionalProperties: false 和 required 字段是可以同时存在的,但原始代码的条件顺序导致 additionalProperties 优先匹配,阻止了 required 字段的正确处理。
解决方案
调整条件类型的匹配顺序,将 properties + required 的检查放在 additionalProperties 之前:
type InferFromJSONSchema<T> =
// ... 其他类型 ...
: // 对象类型 - 带 required 字段(必需字段 + 可选字段)
// 注意:必须在 additionalProperties 检查之前,因为两者可能同时存在
T extends { type: "object"; properties: infer P; required: infer R extends readonly string[] }
? {
[K in keyof P as K extends R[number] ? K : never]: InferFromJSONSchema<P[K]>
} & {
[K in keyof P as K extends R[number] ? never : K]?: InferFromJSONSchema<P[K]>
}
: // 对象类型 - 不带 required 字段(所有字段可选)
T extends { type: "object"; properties: infer P }
? {
[K in keyof P]?: InferFromJSONSchema<P[K]>
}
: // 对象类型 - 只有 additionalProperties(纯动态对象)
T extends { type: "object"; additionalProperties: infer A }
? (A extends false ? Record<string, never> : Record<string, any>)
: // ...
关键改进
- 优先级调整:将
properties + required的检查提前 - additionalProperties 处理优化:
A extends false→Record<string, never>(空对象)- 其他情况 →
Record<string, any>(动态对象)
验证结果
测试场景 1: 带 required + additionalProperties: false
const schema = {
"type": "object",
"properties": {
"id": { "type": "string" },
"domain": { "type": "string" },
"appId": { "type": "string" },
"status": { "type": "string", "enum": ["active", "inactive"] }
},
"required": ["domain"],
"additionalProperties": false
} as const;
推断结果:
type Result = {
domain: string; // ✓ 必需字段
id?: string; // ✓ 可选字段
appId?: string; // ✓ 可选字段
status?: "active" | "inactive"; // ✓ 可选字段 + enum 推断正确
}
测试场景 2: 所有字段可选
const schema = {
"type": "object",
"properties": {
"id": { "type": "string" },
"domain": { "type": "string" }
},
"additionalProperties": false
} as const;
推断结果:
type Result = {
id?: string; // ✓ 可选
domain?: string; // ✓ 可选
}
测试场景 3: 所有字段必需
const schema = {
"type": "object",
"properties": {
"id": { "type": "string" },
"domain": { "type": "string" }
},
"required": ["id", "domain"]
} as const;
推断结果:
type Result = {
id: string; // ✓ 必需
domain: string; // ✓ 必需
}
TypeScript 类型系统的关键知识点
1. as const 的作用
使用 as const 后:
required: ["domain"]的类型从string[]变为readonly ["domain"]R[number]从string(匹配所有字符串键)变为"domain"(只匹配字面量)- 这使得
K extends R[number]的判断更加精确
2. 条件类型的匹配顺序
TypeScript 的条件类型是从上到下匹配的,第一个匹配的条件会被使用。因此:
- 更具体的条件应该放在前面
- 更通用的条件应该放在后面
- 可能同时满足多个条件时,要考虑优先级
3. extends 的结构类型检查
T extends { type: "object"; additionalProperties: infer A }
即使 T 还有其他字段(如 properties、required),只要它包含 type 和 additionalProperties,这个条件就会匹配成功。
总结
这个问题的核心是 TypeScript 条件类型的匹配顺序问题。当 JSON Schema 同时包含多个字段时,需要确保更具体的条件(如 properties + required)在更通用的条件(如只检查 additionalProperties)之前被检查。
修复后的类型系统现在能够正确处理:
- ✅ 必需字段和可选字段的区分
- ✅
as const的字面量类型推断 - ✅
enum字段的联合类型推断 - ✅
additionalProperties的正确处理 - ✅ 各种 JSON Schema 字段组合