Files
query/docs/required-field-inference.md

4.6 KiB
Raw Permalink Blame History

Query API 类型推断优化 - 支持 JSON Schema Required 字段

问题描述

之前的 query-api 实现对所有参数使用 Partial,导致即使 JSON Schema 中定义了 required 字段TypeScript 类型推断也会将所有字段标记为可选。

示例:

const api = {
  update: {
    metadata: {
      args: {
        data: {
          type: "object",
          properties: {
            id: { type: "string" },
            domain: { type: "string" }
          },
          required: ["domain"]  // 只有 domain 是必需的
        }
      }
    }
  }
} as const;

// 之前:所有字段都是可选的
// queryApi.update({ data: {} }) // 不会报错,但应该要求 domain

// 现在:正确识别必需字段
queryApi.update({ data: {} }) // 报错:缺少必需字段 "domain"
queryApi.update({ data: { domain: "test.com" } }) // 正确

解决方案

1. 增强 InferFromJSONSchema 类型

更新类型推断逻辑以支持 required 字段:

type InferFromJSONSchema<T> =
  // ... 其他类型处理 ...
  
  // 对象类型 - 带 required 字段(必需字段 + 可选字段)
  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]>
  }
  : ...

2. 移除不必要的 Partial

从以下位置移除 Partial 包装:

  • ApiMethods 类型定义
  • QueryApi.post() 方法
  • createApi() 方法内部

3. 支持 additionalProperties

添加对动态对象的支持:

// 对象类型 - additionalProperties
T extends { type: "object"; additionalProperties: infer A }
? (A extends {} ? Record<string, any> : never)
: ...

类型推断示例

示例 1必需字段和可选字段

const api = {
  update: {
    metadata: {
      args: {
        data: {
          type: "object",
          properties: {
            id: { type: "string" },
            domain: { type: "string" },
            status: { 
              type: "string",
              enum: ["active", "inactive"]
            }
          },
          required: ["domain"]
        }
      }
    }
  }
} as const;

const queryApi = createQueryApi({ api });

// ✅ 正确:只传必需字段
queryApi.update({ data: { domain: "test.com" } });

// ✅ 正确:传必需字段 + 可选字段
queryApi.update({ 
  data: { 
    domain: "test.com", 
    id: "123",
    status: "active"
  } 
});

// ❌ 错误:缺少必需字段
queryApi.update({ data: { id: "123" } }); 
// Error: 类型 "{ id: string; }" 中缺少属性 "domain"

// ❌ 错误enum 值不正确
queryApi.update({ 
  data: { 
    domain: "test.com", 
    status: "pending" 
  } 
}); 
// Error: 不能将类型 "pending" 分配给类型 "active" | "inactive"

示例 2没有 required 字段(全部可选)

const api = {
  list: {
    metadata: {
      args: {
        data: {
          type: "object",
          properties: {
            page: { type: "number" },
            pageSize: { type: "number" }
          }
          // 没有 required 字段
        }
      }
    }
  }
} as const;

const queryApi = createQueryApi({ api });

// ✅ 所有字段都是可选的
queryApi.list({ data: {} });
queryApi.list({ data: { page: 1 } });
queryApi.list({ data: { page: 1, pageSize: 20 } });

示例 3动态对象 (additionalProperties)

const api = {
  createMeta: {
    metadata: {
      args: {
        metadata: {
          type: "object",
          additionalProperties: {}  // 动态键值对
        }
      }
    }
  }
} as const;

const queryApi = createQueryApi({ api });

// ✅ 可以传入任意键值对
queryApi.createMeta({ metadata: { key1: "value1", key2: 123 } });

测试

运行测试文件验证类型推断:

npx tsc --noEmit test/verify-fix.ts

文件修改

  • src/query-api.ts - 更新类型推断逻辑
  • test/verify-fix.ts - 测试用例
  • test/type-test.ts - 类型推断测试
  • test/debug-type.ts - 调试类型推断

注意事项

  1. as const - API 定义必须使用 as const 以保持字面量类型
  2. readonly - 类型推断需要处理 readonly 修饰符
  3. enum - 自动推断 enum 值并提供类型约束
  4. 构建 - 修改后需要重新构建项目:npm run build