schema-dsl 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -203,6 +203,79 @@ function getDefaultValidator() {
203
203
  return _defaultValidator;
204
204
  }
205
205
 
206
+ /**
207
+ * 智能类型转换:只转换字符串→数字(当schema要求number且能转换时)
208
+ * @private
209
+ * @param {*} data - 原始数据
210
+ * @param {Object} schema - JSON Schema对象
211
+ * @returns {*} 转换后的数据
212
+ */
213
+ function smartCoerceTypes(data, schema) {
214
+ if (!data || typeof data !== 'object') return data;
215
+
216
+ // 获取 schema 对象
217
+ const schemaObj = schema.toSchema ? schema.toSchema() : schema;
218
+ const properties = schemaObj.properties || {};
219
+
220
+ // 处理数组
221
+ if (Array.isArray(data)) {
222
+ return data.map(item => smartCoerceTypes(item, schema));
223
+ }
224
+
225
+ // 处理对象
226
+ const result = { ...data };
227
+
228
+ Object.keys(result).forEach(key => {
229
+ const value = result[key];
230
+ const fieldSchema = properties[key];
231
+
232
+ if (!fieldSchema) return;
233
+
234
+ // ⚠️ 关键修复:如果字段有 enum 约束,不进行类型转换
235
+ // 原因:枚举验证需要严格匹配类型
236
+ // 例如:数字枚举 [1,2,3] 不应该接受字符串 "1"
237
+ if (fieldSchema.enum) {
238
+ return; // 跳过枚举字段的转换
239
+ }
240
+
241
+ // 核心规则:只有同时满足以下三个条件才转换
242
+ // 1. 值是字符串
243
+ // 2. Schema 要求 number 类型
244
+ // 3. 能正常转换为有效数字
245
+ // 4. 不是枚举字段(已在上面检查)
246
+ if (fieldSchema.type === 'number' && typeof value === 'string') {
247
+ const trimmed = value.trim();
248
+ if (trimmed !== '') {
249
+ const num = Number(trimmed);
250
+ if (!isNaN(num)) {
251
+ result[key] = num;
252
+ }
253
+ }
254
+ }
255
+ // 处理嵌套对象
256
+ else if (fieldSchema.type === 'object' && typeof value === 'object' && value !== null) {
257
+ result[key] = smartCoerceTypes(value, fieldSchema);
258
+ }
259
+ // 处理数组元素
260
+ else if (fieldSchema.type === 'array' && Array.isArray(value)) {
261
+ if (fieldSchema.items && fieldSchema.items.type === 'number') {
262
+ result[key] = value.map(item => {
263
+ if (typeof item === 'string') {
264
+ const trimmed = item.trim();
265
+ if (trimmed !== '') {
266
+ const num = Number(trimmed);
267
+ return !isNaN(num) ? num : item;
268
+ }
269
+ }
270
+ return item;
271
+ });
272
+ }
273
+ }
274
+ });
275
+
276
+ return result;
277
+ }
278
+
206
279
  /**
207
280
  * 便捷验证方法(使用默认Validator)
208
281
  * @param {Object} schema - JSON Schema对象
@@ -211,6 +284,7 @@ function getDefaultValidator() {
211
284
  * @param {boolean} [options.format=true] - 是否格式化错误
212
285
  * @param {string} [options.locale] - 动态指定语言(如 'zh-CN', 'en-US')
213
286
  * @param {Object} [options.messages] - 自定义错误消息
287
+ * @param {boolean} [options.coerce=true] - 是否启用智能类型转换(字符串→数字)
214
288
  * @returns {Object} 验证结果
215
289
  *
216
290
  * @example
@@ -218,13 +292,24 @@ function getDefaultValidator() {
218
292
  *
219
293
  * const schema = dsl({ email: 'email!' });
220
294
  *
221
- * // 基本验证
222
- * const result1 = validate(schema, { email: 'test@example.com' });
295
+ * // 基本验证(默认启用智能转换)
296
+ * const result1 = validate(schema, { userId: '123', age: '25' });
297
+ * // userId 和 age 自动转为数字
298
+ *
299
+ * // 禁用智能转换
300
+ * const result2 = validate(schema, data, { coerce: false });
223
301
  *
224
302
  * // 指定语言
225
- * const result2 = validate(schema, { email: 'invalid' }, { locale: 'zh-CN' });
303
+ * const result3 = validate(schema, { email: 'invalid' }, { locale: 'zh-CN' });
226
304
  */
227
- function validate(schema, data, options) {
305
+ function validate(schema, data, options = {}) {
306
+ // 默认启用智能转换(只转换字符串→数字)
307
+ const shouldCoerce = options.coerce !== false;
308
+
309
+ if (shouldCoerce) {
310
+ data = smartCoerceTypes(data, schema);
311
+ }
312
+
228
313
  return getDefaultValidator().validate(schema, data, options);
229
314
  }
230
315
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schema-dsl",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "简洁强大的JSON Schema验证库 - DSL语法 + String扩展 + 便捷validate",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -1,298 +0,0 @@
1
- # TypeScript never 类型在 try-catch 中的行为分析
2
-
3
- ## 问题描述
4
-
5
- 当在 `try-catch` 的 `catch` 块中使用 `dsl.error.throw()` 时,TypeScript 可能会报错或者类型推断不正确。
6
-
7
- ## 原因分析
8
-
9
- ### 1. TypeScript 的控制流分析
10
-
11
- TypeScript 的控制流分析(Control Flow Analysis)对 `try-catch` 块有特殊处理:
12
-
13
- ```typescript
14
- function test(): string {
15
- try {
16
- return 'value';
17
- } catch (error) {
18
- // 在 catch 块中,TypeScript 需要确定:
19
- // 1. 这个块是否会正常返回?
20
- // 2. 这个块是否会抛出异常?
21
- // 3. 这个块是否永不返回(never)?
22
- }
23
- // 这里:TypeScript 需要判断是否可达
24
- }
25
- ```
26
-
27
- ### 2. never 类型的特殊性
28
-
29
- `never` 类型表示"永不返回",但在 `try-catch` 中,TypeScript 的推断可能受限:
30
-
31
- **场景A - 直接调用(✅ 正常工作)**:
32
- ```typescript
33
- function directCall(id: string | null) {
34
- if (!id) {
35
- dsl.error.throw('error'); // TypeScript 知道这里 never 返回
36
- }
37
- console.log(id.toUpperCase()); // ✅ id 被收窄为 string
38
- }
39
- ```
40
-
41
- **场景B - 在 catch 中调用(⚠️ 可能有问题)**:
42
- ```typescript
43
- function inCatch(id: string | null): string {
44
- try {
45
- if (!id) throw new Error();
46
- return id.toUpperCase();
47
- } catch (error) {
48
- dsl.error.throw('error'); // ⚠️ TypeScript 可能不确定这里 never 返回
49
- }
50
- // ❌ 这里可能被认为是可达的
51
- }
52
- ```
53
-
54
- ### 3. 为什么添加 unreachable 保证有效?
55
-
56
- 我们的修复方案(添加第二个 throw)帮助 TypeScript 更明确地理解函数行为:
57
-
58
- **修复前**:
59
- ```javascript
60
- static throw(code, paramsOrLocale, statusCode, locale) {
61
- throw new I18nError(...);
62
- // TypeScript: "函数体结束了,但我不确定是否永不返回"
63
- }
64
- ```
65
-
66
- **修复后**:
67
- ```javascript
68
- static throw(code, paramsOrLocale, statusCode, locale) {
69
- throw new I18nError(...);
70
- // eslint-disable-next-line no-unreachable
71
- throw new Error('unreachable');
72
- // TypeScript: "哦,有两个 throw,这个函数确实永不返回"
73
- }
74
- ```
75
-
76
- ## 测试结果
77
-
78
- ### 当前版本 (v1.2.0) - 所有场景通过 ✅
79
-
80
- 经过测试,以下所有场景都**不再报错**:
81
-
82
- | 场景 | 描述 | 结果 |
83
- |------|------|------|
84
- | scenario1 | 直接使用 throw | ✅ 通过 |
85
- | scenario2 | catch 块中使用 throw(无返回值) | ✅ 通过 |
86
- | scenario3 | catch 块中使用 throw(有返回类型) | ✅ 通过 |
87
- | scenario4 | catch 块中 throw 后有其他代码 | ✅ 通过 |
88
- | scenario5 | 使用类型断言 | ✅ 通过 |
89
- | scenario6 | 多层 try-catch 嵌套 | ✅ 通过 |
90
- | scenario7 | 普通 throw 对比 | ✅ 通过 |
91
- | scenario8 | 返回 never 函数 | ✅ 通过 |
92
- | scenario9 | 严格模式无 return | ✅ 通过 |
93
- | scenario10 | catch 后有代码块 | ✅ 通过 |
94
- | scenario11 | 条件分支中的 throw | ✅ 通过 |
95
- | scenario12 | 类型收窄测试 | ✅ 通过 |
96
-
97
- ## 为什么会报错(理论情况)
98
-
99
- 在某些 TypeScript 配置或版本下,可能出现以下报错:
100
-
101
- ### 错误1: "Function lacks ending return statement"
102
-
103
- ```typescript
104
- function test(): string {
105
- try {
106
- return 'value';
107
- } catch (error) {
108
- dsl.error.throw('error');
109
- }
110
- // TS2366: Function lacks ending return statement and return type does not include 'undefined'.
111
- }
112
- ```
113
-
114
- **原因**: TypeScript 不确定 catch 块是否永不返回
115
-
116
- ### 错误2: "Not all code paths return a value"
117
-
118
- ```typescript
119
- function test(): string {
120
- try {
121
- return 'value';
122
- } catch (error) {
123
- dsl.error.throw('error');
124
- }
125
- console.log('after catch');
126
- // TS7030: Not all code paths return a value.
127
- }
128
- ```
129
-
130
- **原因**: TypeScript 认为 catch 块可能正常结束,导致后面的代码可达
131
-
132
- ### 错误3: 类型收窄失效
133
-
134
- ```typescript
135
- function test(value: string | null) {
136
- try {
137
- if (!value) {
138
- throw new Error();
139
- }
140
- return value.toUpperCase();
141
- } catch (error) {
142
- dsl.error.throw('error');
143
- }
144
- // value 在这里可能仍然是 string | null,而不是 string
145
- }
146
- ```
147
-
148
- **原因**: TypeScript 的控制流分析在 try-catch 边界可能失效
149
-
150
- ## 解决方案对比
151
-
152
- ### 方案1: 我们的修复(推荐)✅
153
-
154
- ```javascript
155
- // lib/errors/I18nError.js
156
- static throw(code, paramsOrLocale, statusCode, locale) {
157
- throw new I18nError(code, params, actualStatusCode, actualLocale);
158
- // eslint-disable-next-line no-unreachable
159
- throw new Error('unreachable');
160
- }
161
- ```
162
-
163
- **优点**:
164
- - ✅ 修复了类型系统问题
165
- - ✅ 不影响运行时行为
166
- - ✅ 不需要用户修改代码
167
-
168
- ### 方案2: 用户使用类型断言(临时方案)
169
-
170
- ```typescript
171
- function test(): string {
172
- try {
173
- return 'value';
174
- } catch (error) {
175
- dsl.error.throw('error');
176
- return undefined as never; // 类型断言
177
- }
178
- }
179
- ```
180
-
181
- **缺点**:
182
- - ❌ 需要用户每次都写
183
- - ❌ 代码不优雅
184
- - ❌ 容易忘记
185
-
186
- ### 方案3: 使用 return
187
-
188
- ```typescript
189
- function test(): string {
190
- try {
191
- return 'value';
192
- } catch (error) {
193
- return dsl.error.throw('error'); // return 一个 never
194
- }
195
- }
196
- ```
197
-
198
- **缺点**:
199
- - ❌ 语义不清晰(return 一个永不返回的函数?)
200
- - ❌ 用户需要记住这个用法
201
-
202
- ### 方案4: 不使用 try-catch(规避)
203
-
204
- ```typescript
205
- function test(id: string | null): string {
206
- if (!id) {
207
- dsl.error.throw('error'); // ✅ 这样就没问题
208
- }
209
- return id.toUpperCase();
210
- }
211
- ```
212
-
213
- **缺点**:
214
- - ❌ 限制了用户的代码结构
215
- - ❌ 不适用于需要捕获异常的场景
216
-
217
- ## TypeScript 版本兼容性
218
-
219
- | TypeScript 版本 | 修复前 | 修复后 |
220
- |----------------|--------|--------|
221
- | 4.5+ | ⚠️ 可能报错 | ✅ 正常 |
222
- | 4.0-4.4 | ⚠️ 可能报错 | ✅ 正常 |
223
- | 3.x | ⚠️ 可能报错 | ✅ 正常 |
224
-
225
- ## 实际使用建议
226
-
227
- ### ✅ 推荐用法
228
-
229
- ```typescript
230
- // 1. 直接在条件分支中使用(最推荐)
231
- function test1(id: string | null) {
232
- if (!id) {
233
- dsl.error.throw('error');
234
- }
235
- return id.toUpperCase();
236
- }
237
-
238
- // 2. 在 catch 块中使用(v1.2.0+ 支持)
239
- function test2(): string {
240
- try {
241
- return processData();
242
- } catch (error) {
243
- dsl.error.throw('processing.failed'); // ✅ 正常工作
244
- }
245
- }
246
-
247
- // 3. 使用 assert(推荐)
248
- function test3(account: any) {
249
- dsl.error.assert(account, 'account.notFound');
250
- return account.balance; // account 已被收窄为 truthy
251
- }
252
- ```
253
-
254
- ### ⚠️ 注意事项
255
-
256
- ```typescript
257
- // 1. 条件分支中的 throw(需要确保所有分支都覆盖)
258
- function test(shouldThrow: boolean): string {
259
- try {
260
- return 'value';
261
- } catch (error) {
262
- if (shouldThrow) {
263
- dsl.error.throw('error');
264
- }
265
- return 'fallback'; // ⚠️ 这个 return 是必需的
266
- }
267
- }
268
-
269
- // 2. finally 块中不要使用 throw
270
- function test(): string {
271
- try {
272
- return 'value';
273
- } catch (error) {
274
- dsl.error.throw('error');
275
- } finally {
276
- // ❌ 不要在这里使用 throw
277
- // dsl.error.throw('finally.error');
278
- }
279
- }
280
- ```
281
-
282
- ## 总结
283
-
284
- 1. **问题根源**: TypeScript 的控制流分析在 try-catch 中对 never 类型的推断可能不够准确
285
-
286
- 2. **修复方案**: 在 throw 方法末尾添加 `throw new Error('unreachable')`,明确告诉 TypeScript 这个函数永不返回
287
-
288
- 3. **修复效果**: v1.2.0 版本后,所有测试场景都通过,不再报错
289
-
290
- 4. **用户影响**: 用户无需修改任何代码,升级到 v1.2.0 即可解决问题
291
-
292
- 5. **向后兼容**: 完全向后兼容,不影响现有功能
293
-
294
- ## 参考资料
295
-
296
- - [TypeScript Control Flow Analysis](https://www.typescriptlang.org/docs/handbook/2/narrowing.html)
297
- - [TypeScript never Type](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-never-type)
298
- - [TypeScript Issue #8655](https://github.com/microsoft/TypeScript/issues/8655) - never in try-catch
@@ -1,183 +0,0 @@
1
- /**
2
- * 测试场景:在 try-catch 中使用 throw 方法
3
- * 验证 TypeScript 类型推断是否正确
4
- */
5
- import { dsl } from './index';
6
-
7
- // ===== 场景1:直接在函数中使用 throw(正常工作)=====
8
- function scenario1_direct(id: string | null) {
9
- if (!id) {
10
- dsl.error.throw('account.notFound');
11
- // ✅ TypeScript 知道这里永不返回
12
- }
13
- // ✅ TypeScript 知道 id 一定不是 null
14
- console.log(id.toUpperCase());
15
- }
16
-
17
- // ===== 场景2:在 try-catch 的 catch 块中使用 throw =====
18
- function scenario2_inCatch(id: string | null) {
19
- try {
20
- // 一些可能抛错的操作
21
- if (!id) {
22
- throw new Error('id is null');
23
- }
24
- return id.toUpperCase();
25
- } catch (error) {
26
- // ⚠️ 在 catch 块中调用 dsl.error.throw
27
- dsl.error.throw('account.notFound');
28
- // ❌ 问题:TypeScript 可能不知道这里永不返回
29
- }
30
- // ❌ TypeScript 可能认为这里可达,报错:"Not all code paths return a value"
31
- }
32
-
33
- // ===== 场景3:在 catch 块中使用 throw,有返回类型声明 =====
34
- function scenario3_withReturnType(id: string | null): string {
35
- try {
36
- if (!id) {
37
- throw new Error('id is null');
38
- }
39
- return id.toUpperCase();
40
- } catch (error) {
41
- dsl.error.throw('account.notFound');
42
- }
43
- // ❌ TypeScript 会报错:"Function lacks ending return statement"
44
- }
45
-
46
- // ===== 场景4:在 catch 块中先 throw,再有其他代码 =====
47
- function scenario4_codeAfterThrow(id: string | null): string {
48
- try {
49
- if (!id) {
50
- throw new Error('id is null');
51
- }
52
- return id.toUpperCase();
53
- } catch (error) {
54
- dsl.error.throw('account.notFound');
55
- console.log('这行代码永远不会执行'); // ⚠️ 但 TypeScript 可能认为会执行
56
- return 'default'; // ⚠️ TypeScript 可能认为这是有效的返回
57
- }
58
- }
59
-
60
- // ===== 场景5:使用类型断言(workaround)=====
61
- function scenario5_withAssertion(id: string | null): string {
62
- try {
63
- if (!id) {
64
- throw new Error('id is null');
65
- }
66
- return id.toUpperCase();
67
- } catch (error) {
68
- dsl.error.throw('account.notFound');
69
- return undefined as never; // 类型断言 workaround
70
- }
71
- }
72
-
73
- // ===== 场景6:多层 try-catch 嵌套 =====
74
- function scenario6_nested(id: string | null): string {
75
- try {
76
- try {
77
- if (!id) {
78
- throw new Error('id is null');
79
- }
80
- return id.toUpperCase();
81
- } catch (innerError) {
82
- dsl.error.throw('account.notFound');
83
- }
84
- } catch (outerError) {
85
- return 'fallback';
86
- }
87
- // ❌ TypeScript 可能报错
88
- }
89
-
90
- // ===== 场景7:对比 - 使用普通 throw(正常工作)=====
91
- function scenario7_normalThrow(id: string | null): string {
92
- try {
93
- if (!id) {
94
- throw new Error('id is null');
95
- }
96
- return id.toUpperCase();
97
- } catch (error) {
98
- throw new Error('account not found'); // ✅ TypeScript 知道这里永不返回
99
- }
100
- // ✅ 不会报错
101
- }
102
-
103
- // ===== 场景8:在 catch 中返回 never 类型的函数调用 =====
104
- function throwHelper(): never {
105
- throw new Error('helper');
106
- }
107
-
108
- function scenario8_helperFunction(id: string | null): string {
109
- try {
110
- if (!id) {
111
- throw new Error('id is null');
112
- }
113
- return id.toUpperCase();
114
- } catch (error) {
115
- return throwHelper(); // ⚠️ return 一个 never 类型的函数
116
- }
117
- // ✅ 可能不报错,但这不是正确的用法
118
- }
119
-
120
- // ===== 场景9:严格模式 - 没有 return 语句 =====
121
- function scenario9_strictNoReturn(id: string | null): string {
122
- try {
123
- if (!id) {
124
- throw new Error('id is null');
125
- }
126
- return id.toUpperCase();
127
- } catch (error) {
128
- dsl.error.throw('account.notFound');
129
- }
130
- // 在这里没有 return 语句
131
- }
132
-
133
- // ===== 场景10:catch 后面有额外代码块 =====
134
- function scenario10_codeAfterCatch(id: string | null): string {
135
- try {
136
- if (!id) {
137
- throw new Error('id is null');
138
- }
139
- return id.toUpperCase();
140
- } catch (error) {
141
- dsl.error.throw('account.notFound');
142
- }
143
-
144
- // 这里有代码
145
- console.log('after catch');
146
- return 'default';
147
- }
148
-
149
- // ===== 场景11:条件分支中的 throw =====
150
- function scenario11_conditionalThrow(id: string | null, shouldThrow: boolean): string {
151
- try {
152
- if (!id) {
153
- throw new Error('id is null');
154
- }
155
- return id.toUpperCase();
156
- } catch (error) {
157
- if (shouldThrow) {
158
- dsl.error.throw('account.notFound');
159
- }
160
- return 'fallback'; // 这个 return 是必需的
161
- }
162
- }
163
-
164
- // ===== 场景12:测试类型收窄 =====
165
- function scenario12_typeNarrowing(value: string | number | null): string {
166
- try {
167
- if (value === null) {
168
- throw new Error('value is null');
169
- }
170
-
171
- if (typeof value === 'number') {
172
- return value.toString();
173
- }
174
-
175
- return value.toUpperCase();
176
- } catch (error) {
177
- dsl.error.throw('validation.failed');
178
- }
179
- }
180
-
181
- console.log('✅ 如果编译成功,说明所有场景的类型都正确');
182
-
183
-