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

202 lines
4.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Query API 类型推断优化 - 支持 JSON Schema Required 字段
## 问题描述
之前的 `query-api` 实现对所有参数使用 `Partial`,导致即使 JSON Schema 中定义了 `required` 字段TypeScript 类型推断也会将所有字段标记为可选。
示例:
```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` 字段:
```typescript
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
添加对动态对象的支持:
```typescript
// 对象类型 - additionalProperties
T extends { type: "object"; additionalProperties: infer A }
? (A extends {} ? Record<string, any> : never)
: ...
```
## 类型推断示例
### 示例 1必需字段和可选字段
```typescript
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 字段(全部可选)
```typescript
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)
```typescript
const api = {
createMeta: {
metadata: {
args: {
metadata: {
type: "object",
additionalProperties: {} // 动态键值对
}
}
}
}
} as const;
const queryApi = createQueryApi({ api });
// ✅ 可以传入任意键值对
queryApi.createMeta({ metadata: { key1: "value1", key2: 123 } });
```
## 测试
运行测试文件验证类型推断:
```bash
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`