chore: bump version to 0.0.43, enhance JSON Schema type inference, and add usage examples
This commit is contained in:
197
docs/json-schema-inference.md
Normal file
197
docs/json-schema-inference.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# JSON Schema 类型推断优化说明
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
|
||||||
|
之前在使用 `createQueryApi` 时,如果 API 定义使用 JSON Schema(而不是 Zod schema),参数类型会被推断为 `unknown`,导致失去类型安全性。
|
||||||
|
|
||||||
|
## 优化方案
|
||||||
|
|
||||||
|
### 修改内容
|
||||||
|
|
||||||
|
在 `src/query-api.ts` 中增强了 `InferFromJSONSchema` 和 `InferType` 类型,使其能够正确处理:
|
||||||
|
|
||||||
|
1. **嵌套对象的 JSON Schema**
|
||||||
|
2. **带有 `$schema` 字段的 JSON Schema**
|
||||||
|
3. **没有 `type` 字段但有 `properties` 字段的对象**
|
||||||
|
|
||||||
|
### 核心改进
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 增强的 InferFromJSONSchema 类型
|
||||||
|
type InferFromJSONSchema<T> =
|
||||||
|
// ... 基础类型处理 ...
|
||||||
|
// 新增:处理没有 type 但有 properties 的对象
|
||||||
|
T extends Record<string, any>
|
||||||
|
? T extends { properties: infer P }
|
||||||
|
? { [K in keyof P]: InferFromJSONSchema<P[K]> }
|
||||||
|
: unknown
|
||||||
|
: unknown;
|
||||||
|
|
||||||
|
// 增强的 InferType 类型
|
||||||
|
type InferType<T> =
|
||||||
|
T extends z.ZodType<infer U> ? U :
|
||||||
|
T extends { type: infer TType } ? InferFromJSONSchema<T> :
|
||||||
|
T extends { properties: infer P } ? InferFromJSONSchema<T> : // 新增
|
||||||
|
T;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 示例 1: 基础 JSON Schema
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const api = {
|
||||||
|
"user": {
|
||||||
|
"get": {
|
||||||
|
"path": "user",
|
||||||
|
"key": "get",
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"userId": { "type": "string" },
|
||||||
|
"includeProfile": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const queryApi = createQueryApi({ api });
|
||||||
|
|
||||||
|
// ✅ 完整的类型推断
|
||||||
|
queryApi.user.get({
|
||||||
|
userId: '123', // string
|
||||||
|
includeProfile: true // boolean
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 2: 嵌套对象(你的场景)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const api = {
|
||||||
|
"app_domain_manager": {
|
||||||
|
"get": {
|
||||||
|
"path": "app_domain_manager",
|
||||||
|
"key": "get",
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"data": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"domain": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const queryApi = createQueryApi({ api });
|
||||||
|
|
||||||
|
// ✅ data 参数被正确推断为 { id: string, domain: string }
|
||||||
|
queryApi.app_domain_manager.get({
|
||||||
|
data: {
|
||||||
|
id: '123',
|
||||||
|
domain: 'example.com'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 3: 枚举类型
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const api = {
|
||||||
|
"order": {
|
||||||
|
"updateStatus": {
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["pending", "processing", "shipped"] as const
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const queryApi = createQueryApi({ api });
|
||||||
|
|
||||||
|
// ✅ status 被推断为 "pending" | "processing" | "shipped"
|
||||||
|
queryApi.order.updateStatus({
|
||||||
|
status: 'shipped' // 自动补全和类型检查
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 4: 复杂嵌套
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const api = {
|
||||||
|
"product": {
|
||||||
|
"create": {
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"product": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"price": { "type": "number" },
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"color": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const queryApi = createQueryApi({ api });
|
||||||
|
|
||||||
|
// ✅ 深度嵌套的类型推断
|
||||||
|
queryApi.product.create({
|
||||||
|
product: {
|
||||||
|
name: 'T-Shirt', // string
|
||||||
|
price: 29.99, // number
|
||||||
|
tags: ['summer'], // string[]
|
||||||
|
metadata: {
|
||||||
|
color: 'blue' // string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 支持的 JSON Schema 特性
|
||||||
|
|
||||||
|
- ✅ 基础类型: `string`, `number`, `integer`, `boolean`
|
||||||
|
- ✅ 对象类型: `type: "object"` with `properties`
|
||||||
|
- ✅ 数组类型: `type: "array"` with `items`
|
||||||
|
- ✅ 枚举类型: `enum` 字段
|
||||||
|
- ✅ 嵌套对象和数组
|
||||||
|
- ✅ 忽略 `$schema` 等元数据字段
|
||||||
|
- ✅ 支持 `as const` 断言以获得更精确的类型
|
||||||
|
|
||||||
|
## 类型安全
|
||||||
|
|
||||||
|
优化后,TypeScript 会:
|
||||||
|
- ✅ 提供完整的自动补全
|
||||||
|
- ✅ 在编译时检测类型错误
|
||||||
|
- ✅ 防止传入不存在的属性
|
||||||
|
- ✅ 确保值的类型正确
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
运行测试文件验证类型推断:
|
||||||
|
```bash
|
||||||
|
bun test/json-schema-examples.ts
|
||||||
|
```
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/query",
|
"name": "@kevisual/query",
|
||||||
"version": "0.0.42",
|
"version": "0.0.43",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run clean && bun run bun.config.ts",
|
"build": "npm run clean && bun run bun.config.ts",
|
||||||
|
|||||||
@@ -13,22 +13,34 @@ type Pos = {
|
|||||||
|
|
||||||
// JSON Schema 类型推断 - 使用更精确的类型匹配
|
// JSON Schema 类型推断 - 使用更精确的类型匹配
|
||||||
type InferFromJSONSchema<T> =
|
type InferFromJSONSchema<T> =
|
||||||
|
// 处理带 enum 的字符串类型
|
||||||
T extends { type: "string"; enum: readonly (infer E)[] } ? E :
|
T extends { type: "string"; enum: readonly (infer E)[] } ? E :
|
||||||
T extends { type: "string"; enum: (infer E)[] } ? E :
|
T extends { type: "string"; enum: (infer E)[] } ? E :
|
||||||
|
// 基础类型
|
||||||
T extends { type: "string" } ? string :
|
T extends { type: "string" } ? string :
|
||||||
T extends { type: "number" } ? number :
|
T extends { type: "number" } ? number :
|
||||||
T extends { type: "integer" } ? number :
|
T extends { type: "integer" } ? number :
|
||||||
T extends { type: "boolean" } ? boolean :
|
T extends { type: "boolean" } ? boolean :
|
||||||
|
// 对象类型 - 检查是否有 properties
|
||||||
T extends { type: "object"; properties: infer P }
|
T extends { type: "object"; properties: infer P }
|
||||||
? { [K in keyof P]: InferFromJSONSchema<P[K]> }
|
? {
|
||||||
: T extends { type: "array"; items: infer I }
|
[K in keyof P]: InferFromJSONSchema<P[K]>
|
||||||
|
}
|
||||||
|
: // 数组类型
|
||||||
|
T extends { type: "array"; items: infer I }
|
||||||
? Array<InferFromJSONSchema<I>>
|
? Array<InferFromJSONSchema<I>>
|
||||||
|
: // 默认情况 - 如果是对象但没有 type 字段,尝试递归推断
|
||||||
|
T extends Record<string, any>
|
||||||
|
? T extends { properties: infer P }
|
||||||
|
? { [K in keyof P]: InferFromJSONSchema<P[K]> }
|
||||||
|
: unknown
|
||||||
: unknown;
|
: unknown;
|
||||||
|
|
||||||
// 统一类型推断:支持 Zod schema 和原始 JSON Schema
|
// 统一类型推断:支持 Zod schema 和原始 JSON Schema
|
||||||
type InferType<T> =
|
type InferType<T> =
|
||||||
T extends z.ZodType<infer U> ? U : // Zod schema
|
T extends z.ZodType<infer U> ? U : // Zod schema
|
||||||
T extends { type: infer TType } ? InferFromJSONSchema<T> : // 任何包含 type 字段的 JSON Schema(忽略 $schema)
|
T extends { type: infer TType } ? InferFromJSONSchema<T> : // 任何包含 type 字段的 JSON Schema(忽略 $schema)
|
||||||
|
T extends { properties: infer P } ? InferFromJSONSchema<T> : // 处理没有 type 但有 properties 的对象
|
||||||
T;
|
T;
|
||||||
|
|
||||||
// 提取 args 对象,将每个 Zod schema 或 JSON Schema 转换为实际类型
|
// 提取 args 对象,将每个 Zod schema 或 JSON Schema 转换为实际类型
|
||||||
|
|||||||
164
test/json-schema-examples.ts
Normal file
164
test/json-schema-examples.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { createQueryApi } from '../src/query-api.ts';
|
||||||
|
|
||||||
|
// ============ 示例 1: 基础 JSON Schema 推断 ============
|
||||||
|
const api1 = {
|
||||||
|
"user": {
|
||||||
|
"get": {
|
||||||
|
"path": "user",
|
||||||
|
"key": "get",
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"userId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"includeProfile": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const queryApi1 = createQueryApi({ api: api1 });
|
||||||
|
|
||||||
|
// ✅ 类型推断正常工作
|
||||||
|
queryApi1.user.get({
|
||||||
|
userId: '123', // ✅ 推断为 string
|
||||||
|
includeProfile: true // ✅ 推断为 boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
// ❌ TypeScript 会报错
|
||||||
|
// queryApi1.user.get({
|
||||||
|
// userId: 123, // 错误:应该是 string
|
||||||
|
// wrongProp: 'test' // 错误:不存在的属性
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
// ============ 示例 2: 嵌套对象 JSON Schema(你的场景)============
|
||||||
|
const api2 = {
|
||||||
|
"app_domain_manager": {
|
||||||
|
"get": {
|
||||||
|
"path": "app_domain_manager",
|
||||||
|
"key": "get",
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"data": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"domain": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const queryApi2 = createQueryApi({ api: api2 });
|
||||||
|
|
||||||
|
// ✅ 类型推断正常工作 - data 参数被推断为 { id: string, domain: string }
|
||||||
|
queryApi2.app_domain_manager.get({
|
||||||
|
data: {
|
||||||
|
id: '123',
|
||||||
|
domain: 'example.com'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ❌ TypeScript 会报错
|
||||||
|
// queryApi2.app_domain_manager.get({
|
||||||
|
// data: {
|
||||||
|
// wrongProp: 'test' // 错误:不存在的属性
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
// ============ 示例 3: 复杂嵌套结构 ============
|
||||||
|
const api3 = {
|
||||||
|
"product": {
|
||||||
|
"create": {
|
||||||
|
"path": "product",
|
||||||
|
"key": "create",
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"product": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"price": { "type": "number" },
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"color": { "type": "string" },
|
||||||
|
"size": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const queryApi3 = createQueryApi({ api: api3 });
|
||||||
|
|
||||||
|
// ✅ 复杂嵌套类型推断
|
||||||
|
queryApi3.product.create({
|
||||||
|
product: {
|
||||||
|
name: 'T-Shirt',
|
||||||
|
price: 29.99,
|
||||||
|
tags: ['clothing', 'summer'],
|
||||||
|
metadata: {
|
||||||
|
color: 'blue',
|
||||||
|
size: 'M'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ============ 示例 4: 枚举类型 ============
|
||||||
|
const api4 = {
|
||||||
|
"order": {
|
||||||
|
"updateStatus": {
|
||||||
|
"path": "order",
|
||||||
|
"key": "updateStatus",
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"orderId": { "type": "string" },
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["pending", "processing", "shipped", "delivered"] as const
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const queryApi4 = createQueryApi({ api: api4 });
|
||||||
|
|
||||||
|
// ✅ 枚举类型推断
|
||||||
|
queryApi4.order.updateStatus({
|
||||||
|
orderId: 'ORD-123',
|
||||||
|
status: 'shipped' // ✅ 自动提示: "pending" | "processing" | "shipped" | "delivered"
|
||||||
|
});
|
||||||
|
|
||||||
|
// ❌ TypeScript 会报错
|
||||||
|
// queryApi4.order.updateStatus({
|
||||||
|
// orderId: 'ORD-123',
|
||||||
|
// status: 'invalid-status' // 错误:不在枚举值中
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
console.log('✅ 所有类型推断示例运行成功!');
|
||||||
@@ -10,34 +10,34 @@ console.log("JSON Schema for the person object:");
|
|||||||
console.log(
|
console.log(
|
||||||
JSON.stringify(toJSONSchema(schema), null, 2)
|
JSON.stringify(toJSONSchema(schema), null, 2)
|
||||||
);
|
);
|
||||||
const jsonSchema = toJSONSchema(schema);
|
// const jsonSchema = toJSONSchema(schema);
|
||||||
|
|
||||||
const schema2 = z.fromJSONSchema(jsonSchema);
|
// const schema2 = z.fromJSONSchema(jsonSchema);
|
||||||
|
|
||||||
// schema2 的类型是 ZodSchema<any>,所以无法在编译时推断出具体类型
|
// // schema2 的类型是 ZodSchema<any>,所以无法在编译时推断出具体类型
|
||||||
// 这是 fromJSONSchema 的限制 - JSON Schema 转换会丢失 TypeScript 类型信息
|
// // 这是 fromJSONSchema 的限制 - JSON Schema 转换会丢失 TypeScript 类型信息
|
||||||
|
|
||||||
schema2.parse({
|
// schema2.parse({
|
||||||
name: "John Doe",
|
// name: "John Doe",
|
||||||
age: 30, // 添加必需的 age 字段
|
// age: 30, // 添加必需的 age 字段
|
||||||
email: "",
|
// email: "",
|
||||||
})
|
// })
|
||||||
|
|
||||||
type Schema2Type = z.infer<typeof schema2>;
|
// type Schema2Type = z.infer<typeof schema2>;
|
||||||
// Schema2Type 被推断为 any
|
// // Schema2Type 被推断为 any
|
||||||
|
|
||||||
// 对比:原始 schema 的类型推断是正常的
|
// // 对比:原始 schema 的类型推断是正常的
|
||||||
type OriginalSchemaType = z.infer<typeof schema>;
|
// type OriginalSchemaType = z.infer<typeof schema>;
|
||||||
// OriginalSchemaType = { name: string; age: number; email?: string | undefined }
|
// // OriginalSchemaType = { name: string; age: number; email?: string | undefined }
|
||||||
|
|
||||||
const v: Schema2Type = {
|
// const v: Schema2Type = {
|
||||||
name: "John Doe",
|
// name: "John Doe",
|
||||||
email: ""
|
// email: ""
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 如果使用原始 schema,类型推断会正常工作:
|
// // 如果使用原始 schema,类型推断会正常工作:
|
||||||
const v2: OriginalSchemaType = {
|
// const v2: OriginalSchemaType = {
|
||||||
name: "John Doe",
|
// name: "John Doe",
|
||||||
age: 30,
|
// age: 30,
|
||||||
// email 是可选的
|
// // email 是可选的
|
||||||
}
|
// }
|
||||||
Reference in New Issue
Block a user