diff --git a/docs/json-schema-inference.md b/docs/json-schema-inference.md new file mode 100644 index 0000000..1fb3904 --- /dev/null +++ b/docs/json-schema-inference.md @@ -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 = + // ... 基础类型处理 ... + // 新增:处理没有 type 但有 properties 的对象 + T extends Record + ? T extends { properties: infer P } + ? { [K in keyof P]: InferFromJSONSchema } + : unknown + : unknown; + +// 增强的 InferType 类型 +type InferType = + T extends z.ZodType ? U : + T extends { type: infer TType } ? InferFromJSONSchema : + T extends { properties: infer P } ? InferFromJSONSchema : // 新增 + 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 +``` diff --git a/package.json b/package.json index 2b0bef2..4649ea8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kevisual/query", - "version": "0.0.42", + "version": "0.0.43", "type": "module", "scripts": { "build": "npm run clean && bun run bun.config.ts", diff --git a/src/query-api.ts b/src/query-api.ts index 50359a7..b7b6282 100644 --- a/src/query-api.ts +++ b/src/query-api.ts @@ -13,22 +13,34 @@ type Pos = { // JSON Schema 类型推断 - 使用更精确的类型匹配 type InferFromJSONSchema = + // 处理带 enum 的字符串类型 T extends { type: "string"; enum: readonly (infer E)[] } ? E : T extends { type: "string"; enum: (infer E)[] } ? E : + // 基础类型 T extends { type: "string" } ? string : T extends { type: "number" } ? number : T extends { type: "integer" } ? number : T extends { type: "boolean" } ? boolean : + // 对象类型 - 检查是否有 properties T extends { type: "object"; properties: infer P } - ? { [K in keyof P]: InferFromJSONSchema } - : T extends { type: "array"; items: infer I } + ? { + [K in keyof P]: InferFromJSONSchema + } + : // 数组类型 + T extends { type: "array"; items: infer I } ? Array> + : // 默认情况 - 如果是对象但没有 type 字段,尝试递归推断 + T extends Record + ? T extends { properties: infer P } + ? { [K in keyof P]: InferFromJSONSchema } + : unknown : unknown; // 统一类型推断:支持 Zod schema 和原始 JSON Schema type InferType = T extends z.ZodType ? U : // Zod schema T extends { type: infer TType } ? InferFromJSONSchema : // 任何包含 type 字段的 JSON Schema(忽略 $schema) + T extends { properties: infer P } ? InferFromJSONSchema : // 处理没有 type 但有 properties 的对象 T; // 提取 args 对象,将每个 Zod schema 或 JSON Schema 转换为实际类型 diff --git a/test/json-schema-examples.ts b/test/json-schema-examples.ts new file mode 100644 index 0000000..9e4ad0c --- /dev/null +++ b/test/json-schema-examples.ts @@ -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('✅ 所有类型推断示例运行成功!'); diff --git a/test/schema.ts b/test/schema.ts index 9896da3..267e65c 100644 --- a/test/schema.ts +++ b/test/schema.ts @@ -10,34 +10,34 @@ console.log("JSON Schema for the person object:"); console.log( 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,所以无法在编译时推断出具体类型 -// 这是 fromJSONSchema 的限制 - JSON Schema 转换会丢失 TypeScript 类型信息 +// // schema2 的类型是 ZodSchema,所以无法在编译时推断出具体类型 +// // 这是 fromJSONSchema 的限制 - JSON Schema 转换会丢失 TypeScript 类型信息 -schema2.parse({ - name: "John Doe", - age: 30, // 添加必需的 age 字段 - email: "", -}) +// schema2.parse({ +// name: "John Doe", +// age: 30, // 添加必需的 age 字段 +// email: "", +// }) -type Schema2Type = z.infer; -// Schema2Type 被推断为 any +// type Schema2Type = z.infer; +// // Schema2Type 被推断为 any -// 对比:原始 schema 的类型推断是正常的 -type OriginalSchemaType = z.infer; -// OriginalSchemaType = { name: string; age: number; email?: string | undefined } +// // 对比:原始 schema 的类型推断是正常的 +// type OriginalSchemaType = z.infer; +// // OriginalSchemaType = { name: string; age: number; email?: string | undefined } -const v: Schema2Type = { - name: "John Doe", - email: "" -} +// const v: Schema2Type = { +// name: "John Doe", +// email: "" +// } -// 如果使用原始 schema,类型推断会正常工作: -const v2: OriginalSchemaType = { - name: "John Doe", - age: 30, - // email 是可选的 -} \ No newline at end of file +// // 如果使用原始 schema,类型推断会正常工作: +// const v2: OriginalSchemaType = { +// name: "John Doe", +// age: 30, +// // email 是可选的 +// } \ No newline at end of file