Files
query/docs/type-inference-fix-report.md

5.8 KiB
Raw Permalink Blame History

类型推断问题修复报告

问题描述

/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.tsInferFromJSONSchema 类型定义中,条件类型的匹配顺序不当

原始代码(有问题的版本)

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: falserequired: ["domain"] 时:

  1. TypeScript 首先尝试匹配第一个条件:T extends { type: "object"; additionalProperties: infer A }
  2. 匹配成功!因为 Schema 确实有 additionalProperties: false
  3. 此时 A = false,而 false extends {}false
  4. 返回 never,导致类型推断失败或行为异常

关键点:在 JSON Schema 规范中,additionalProperties: falserequired 字段是可以同时存在的,但原始代码的条件顺序导致 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>)
  
  : // ...

关键改进

  1. 优先级调整:将 properties + required 的检查提前
  2. additionalProperties 处理优化
    • A extends falseRecord<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 还有其他字段(如 propertiesrequired),只要它包含 typeadditionalProperties,这个条件就会匹配成功。

总结

这个问题的核心是 TypeScript 条件类型的匹配顺序问题。当 JSON Schema 同时包含多个字段时,需要确保更具体的条件(如 properties + required)在更通用的条件(如只检查 additionalProperties)之前被检查。

修复后的类型系统现在能够正确处理:

  • 必需字段和可选字段的区分
  • as const 的字面量类型推断
  • enum 字段的联合类型推断
  • additionalProperties 的正确处理
  • 各种 JSON Schema 字段组合