chore: bump version to 0.0.46 and enhance type inference for JSON Schema required fields

This commit is contained in:
2026-02-18 04:10:43 +08:00
parent 38659cc40e
commit ebfc779857
6 changed files with 739 additions and 13 deletions

View File

@@ -0,0 +1,201 @@
# 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`

View File

@@ -0,0 +1,183 @@
# 类型推断问题修复报告
## 问题描述
`/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](../src/query-api.ts) 的 `InferFromJSONSchema` 类型定义中,**条件类型的匹配顺序不当**。
### 原始代码(有问题的版本)
```typescript
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"]` 时:
1. TypeScript 首先尝试匹配第一个条件:`T extends { type: "object"; additionalProperties: infer A }`
2. 匹配成功!因为 Schema 确实有 `additionalProperties: false`
3. 此时 `A = false`,而 `false extends {}``false`
4. 返回 `never`,导致类型推断失败或行为异常
**关键点**:在 JSON Schema 规范中,`additionalProperties: false``required` 字段是可以同时存在的,但原始代码的条件顺序导致 `additionalProperties` 优先匹配,阻止了 `required` 字段的正确处理。
## 解决方案
调整条件类型的匹配顺序,将 `properties + required` 的检查放在 `additionalProperties` 之前:
```typescript
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 false``Record<string, never>`(空对象)
- 其他情况 → `Record<string, any>`(动态对象)
## 验证结果
### 测试场景 1: 带 required + additionalProperties: false
```typescript
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;
```
**推断结果**
```typescript
type Result = {
domain: string; // ✓ 必需字段
id?: string; // ✓ 可选字段
appId?: string; // ✓ 可选字段
status?: "active" | "inactive"; // ✓ 可选字段 + enum 推断正确
}
```
### 测试场景 2: 所有字段可选
```typescript
const schema = {
"type": "object",
"properties": {
"id": { "type": "string" },
"domain": { "type": "string" }
},
"additionalProperties": false
} as const;
```
**推断结果**
```typescript
type Result = {
id?: string; // ✓ 可选
domain?: string; // ✓ 可选
}
```
### 测试场景 3: 所有字段必需
```typescript
const schema = {
"type": "object",
"properties": {
"id": { "type": "string" },
"domain": { "type": "string" }
},
"required": ["id", "domain"]
} as const;
```
**推断结果**
```typescript
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` 的结构类型检查
```typescript
T extends { type: "object"; additionalProperties: infer A }
```
即使 `T` 还有其他字段(如 `properties``required`),只要它包含 `type``additionalProperties`,这个条件就会匹配成功。
## 总结
这个问题的核心是 **TypeScript 条件类型的匹配顺序问题**。当 JSON Schema 同时包含多个字段时,需要确保更具体的条件(如 `properties + required`)在更通用的条件(如只检查 `additionalProperties`)之前被检查。
修复后的类型系统现在能够正确处理:
- ✅ 必需字段和可选字段的区分
-`as const` 的字面量类型推断
-`enum` 字段的联合类型推断
-`additionalProperties` 的正确处理
- ✅ 各种 JSON Schema 字段组合