202 lines
4.6 KiB
Markdown
202 lines
4.6 KiB
Markdown
# 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`
|