schema-dsl 1.2.0 → 1.2.1
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/CHANGELOG.md +3 -43
- package/docs/typescript-never-in-try-catch.md +298 -0
- package/index.js +1 -6
- package/lib/errors/I18nError.js +0 -3
- package/package.json +1 -1
- package/test-try-catch-scenario.ts +183 -0
- package/changelogs/v1.2.0.md +0 -220
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# 变更日志 (CHANGELOG)
|
|
2
2
|
|
|
3
3
|
> **说明**: 版本概览摘要,详细变更见 [changelogs/](./changelogs/) 目录
|
|
4
|
-
> **最后更新**: 2026-
|
|
4
|
+
> **最后更新**: 2026-01-30
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
| 版本 | 日期 | 变更摘要 | 详细 |
|
|
11
11
|
|------|------|---------|------|
|
|
12
|
-
| [v1.2.0](./changelogs/v1.2.0.md) | 2026-02-02 | 🔧 TypeScript修复:never类型保证 - 修复 throw 方法类型检查 | [查看](./changelogs/v1.2.0.md) |
|
|
13
12
|
| [v1.1.8](./changelogs/v1.1.8.md) | 2026-01-30 | 🚀 新功能:智能参数识别 - 支持简化语法 `dsl.error.throw('key', 'locale')` | [查看](./changelogs/v1.1.8.md) |
|
|
14
13
|
| [v1.1.7](./changelogs/v1.1.7.md) | 2026-01-27 | 🐛 Bug修复:错误消息路径显示优化 - 所有错误类型的 message 只显示字段名 | [查看](./changelogs/v1.1.7.md) |
|
|
15
14
|
| [v1.1.6](./changelogs/v1.1.6.md) | 2026-01-23 | 🐛 Bug修复:enum和additionalProperties错误消息模板变量未替换 | [查看](./changelogs/v1.1.6.md) |
|
|
@@ -31,7 +30,7 @@
|
|
|
31
30
|
| [v1.0.0](./changelogs/v1.0.0.md) | 2025-12-29 | 初始发布版本 | [查看](./changelogs/v1.0.0.md) |
|
|
32
31
|
|
|
33
32
|
> 💡 **提示**: 重要版本的详细变更记录在 [changelogs/](./changelogs/) 目录中。
|
|
34
|
-
> 当前已有详细文档的版本:v1.
|
|
33
|
+
> 当前已有详细文档的版本:v1.1.8, v1.1.7, v1.1.6, v1.1.5, v1.1.4, v1.1.3, v1.1.2, v1.1.1, v1.1.0, v1.0.9, v1.0.0
|
|
35
34
|
> 其他版本的详细变更信息请参考项目提交历史或联系维护者。
|
|
36
35
|
|
|
37
36
|
---
|
|
@@ -40,52 +39,13 @@
|
|
|
40
39
|
|
|
41
40
|
| 版本系列 | 版本数 | 主要改进方向 |
|
|
42
41
|
|---------|-------|------------|
|
|
43
|
-
| v1.
|
|
44
|
-
| v1.1.x | 9 | 智能参数识别、Bug修复、错误消息优化、错误配置增强、多语言支持、数字运算符、联合类型 |
|
|
42
|
+
| v1.1.x | 9 | 智能参数识别、Bug修复、错误消息优化、错误配置增强、TypeScript类型完善、多语言支持、数字运算符、联合类型 |
|
|
45
43
|
| v1.0.x | 10 | 核心功能、验证器、测试覆盖、文档完善 |
|
|
46
44
|
|
|
47
45
|
---
|
|
48
46
|
|
|
49
47
|
## 里程碑版本
|
|
50
48
|
|
|
51
|
-
### v1.2.0 - TypeScript never 类型修复 🔧
|
|
52
|
-
|
|
53
|
-
**发布日期**: 2026-02-02
|
|
54
|
-
**重要性**: ⭐⭐⭐
|
|
55
|
-
|
|
56
|
-
**主要改进**:
|
|
57
|
-
- 🔧 修复 TypeScript `never` 类型编译检查问题
|
|
58
|
-
- ✅ 在 `I18nError.throw()` 和 `dsl.error.throw()` 中添加 unreachable 保证
|
|
59
|
-
- ✅ 确保函数永不返回,满足 TypeScript 类型系统要求
|
|
60
|
-
- ✅ 所有 782 个测试通过,零破坏性变更
|
|
61
|
-
- ✅ 向后兼容,不影响现有功能
|
|
62
|
-
|
|
63
|
-
**技术细节**:
|
|
64
|
-
- 在两个 `throw` 方法末尾添加 `throw new Error('unreachable')`
|
|
65
|
-
- 使用 `eslint-disable-next-line no-unreachable` 禁用 ESLint 警告
|
|
66
|
-
- 修改文件:`lib/errors/I18nError.js` (L211), `index.js` (L94-97)
|
|
67
|
-
- `assert` 方法使用 `asserts condition` 类型,无需修改
|
|
68
|
-
|
|
69
|
-
**问题背景**:
|
|
70
|
-
TypeScript 的 `never` 类型表示函数永不返回。虽然函数内部有 `throw` 语句,但编译器需要明确的保证。添加第二个 `throw` 语句(永远不会执行)满足了类型系统的要求。
|
|
71
|
-
|
|
72
|
-
**使用示例**:
|
|
73
|
-
```typescript
|
|
74
|
-
function test(id: string | null) {
|
|
75
|
-
if (!id) {
|
|
76
|
-
dsl.error.throw('account.notFound');
|
|
77
|
-
// ✅ TypeScript 知道这里永不返回
|
|
78
|
-
}
|
|
79
|
-
// ✅ TypeScript 知道 id 一定不是 null,不需要 ! 断言
|
|
80
|
-
console.log(id.toUpperCase());
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
**修复验证**:
|
|
85
|
-
- ✅ TypeScript 编译检查通过(`npx tsc --noEmit`)
|
|
86
|
-
- ✅ 控制流分析正确推断变量类型
|
|
87
|
-
- ✅ 所有现有测试通过
|
|
88
|
-
|
|
89
49
|
### v1.1.8 - 智能参数识别 🚀
|
|
90
50
|
|
|
91
51
|
**发布日期**: 2026-01-30
|
|
@@ -0,0 +1,298 @@
|
|
|
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
|
package/index.js
CHANGED
|
@@ -89,12 +89,7 @@ dsl.error = {
|
|
|
89
89
|
* // 标准语法
|
|
90
90
|
* dsl.error.throw('account.notFound', { id: '123' }, 404, 'zh-CN');
|
|
91
91
|
*/
|
|
92
|
-
throw: (code, paramsOrLocale, statusCode, locale) =>
|
|
93
|
-
I18nError.throw(code, paramsOrLocale, statusCode, locale);
|
|
94
|
-
// TypeScript never 类型保证
|
|
95
|
-
// eslint-disable-next-line no-unreachable
|
|
96
|
-
throw new Error('unreachable');
|
|
97
|
-
},
|
|
92
|
+
throw: (code, paramsOrLocale, statusCode, locale) => I18nError.throw(code, paramsOrLocale, statusCode, locale),
|
|
98
93
|
|
|
99
94
|
/**
|
|
100
95
|
* 断言方法 - 条件不满足时抛错
|
package/lib/errors/I18nError.js
CHANGED
|
@@ -208,9 +208,6 @@ class I18nError extends Error {
|
|
|
208
208
|
static throw(code, paramsOrLocale, statusCode, locale) {
|
|
209
209
|
const { params, statusCode: actualStatusCode, locale: actualLocale } = normalizeParams(paramsOrLocale, statusCode, locale);
|
|
210
210
|
throw new I18nError(code, params, actualStatusCode, actualLocale);
|
|
211
|
-
// TypeScript never 类型保证
|
|
212
|
-
// eslint-disable-next-line no-unreachable
|
|
213
|
-
throw new Error('unreachable');
|
|
214
211
|
}
|
|
215
212
|
|
|
216
213
|
/**
|
package/package.json
CHANGED
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
|
package/changelogs/v1.2.0.md
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
# v1.2.0 变更日志
|
|
2
|
-
|
|
3
|
-
> **发布日期**: 2026-02-02
|
|
4
|
-
> **类型**: 🔧 TypeScript 类型系统修复
|
|
5
|
-
> **重要性**: ⭐⭐⭐ (中等)
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 📋 版本概览
|
|
10
|
-
|
|
11
|
-
本版本修复了 TypeScript `never` 类型的编译检查问题,确保 `throw` 方法的类型定义符合 TypeScript 类型系统要求。
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## 🔧 主要变更
|
|
16
|
-
|
|
17
|
-
### TypeScript never 类型保证
|
|
18
|
-
|
|
19
|
-
**问题描述**:
|
|
20
|
-
- `dsl.error.throw()` 和 `I18nError.throw()` 的返回类型声明为 `never`
|
|
21
|
-
- 虽然函数内部有 `throw` 语句,但 TypeScript 编译器需要明确的"永不返回"保证
|
|
22
|
-
- 在某些场景下可能出现类型检查报错
|
|
23
|
-
|
|
24
|
-
**修复方案**:
|
|
25
|
-
- 在两个 `throw` 方法末尾添加 `throw new Error('unreachable')`
|
|
26
|
-
- 该语句永远不会执行,但满足了 TypeScript 类型系统的要求
|
|
27
|
-
- 使用 `eslint-disable-next-line no-unreachable` 禁用 ESLint 警告
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## 📝 详细变更
|
|
32
|
-
|
|
33
|
-
### 1. 修复 `I18nError.throw()` (lib/errors/I18nError.js)
|
|
34
|
-
|
|
35
|
-
**位置**: 第 211 行
|
|
36
|
-
|
|
37
|
-
**修改前**:
|
|
38
|
-
```javascript
|
|
39
|
-
static throw(code, paramsOrLocale, statusCode, locale) {
|
|
40
|
-
const { params, statusCode: actualStatusCode, locale: actualLocale } = normalizeParams(paramsOrLocale, statusCode, locale);
|
|
41
|
-
throw new I18nError(code, params, actualStatusCode, actualLocale);
|
|
42
|
-
}
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
**修改后**:
|
|
46
|
-
```javascript
|
|
47
|
-
static throw(code, paramsOrLocale, statusCode, locale) {
|
|
48
|
-
const { params, statusCode: actualStatusCode, locale: actualLocale } = normalizeParams(paramsOrLocale, statusCode, locale);
|
|
49
|
-
throw new I18nError(code, params, actualStatusCode, actualLocale);
|
|
50
|
-
// TypeScript never 类型保证
|
|
51
|
-
// eslint-disable-next-line no-unreachable
|
|
52
|
-
throw new Error('unreachable');
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### 2. 修复 `dsl.error.throw()` (index.js)
|
|
57
|
-
|
|
58
|
-
**位置**: 第 94-97 行
|
|
59
|
-
|
|
60
|
-
**修改前**:
|
|
61
|
-
```javascript
|
|
62
|
-
throw: (code, paramsOrLocale, statusCode, locale) => I18nError.throw(code, paramsOrLocale, statusCode, locale),
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**修改后**:
|
|
66
|
-
```javascript
|
|
67
|
-
throw: (code, paramsOrLocale, statusCode, locale) => {
|
|
68
|
-
I18nError.throw(code, paramsOrLocale, statusCode, locale);
|
|
69
|
-
// TypeScript never 类型保证
|
|
70
|
-
// eslint-disable-next-line no-unreachable
|
|
71
|
-
throw new Error('unreachable');
|
|
72
|
-
},
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## 🎯 技术说明
|
|
78
|
-
|
|
79
|
-
### 为什么需要第二个 throw?
|
|
80
|
-
|
|
81
|
-
1. **TypeScript `never` 类型的语义**
|
|
82
|
-
- `never` 表示函数永不返回(总是抛出异常或进入无限循环)
|
|
83
|
-
- 编译器需要静态分析确认函数确实永不返回
|
|
84
|
-
|
|
85
|
-
2. **类型系统的要求**
|
|
86
|
-
- 虽然第一个 `throw` 会立即中断执行
|
|
87
|
-
- 但编译器需要明确的保证,而不是推断
|
|
88
|
-
- 第二个 `throw` 永远不会执行,但满足了类型系统要求
|
|
89
|
-
|
|
90
|
-
3. **控制流分析的好处**
|
|
91
|
-
```typescript
|
|
92
|
-
function test(id: string | null) {
|
|
93
|
-
if (!id) {
|
|
94
|
-
dsl.error.throw('account.notFound');
|
|
95
|
-
// ✅ TypeScript 知道这里永不返回
|
|
96
|
-
}
|
|
97
|
-
// ✅ TypeScript 知道 id 一定不是 null
|
|
98
|
-
console.log(id.toUpperCase()); // 不需要 ! 断言
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### assert 方法为什么不需要修改?
|
|
103
|
-
|
|
104
|
-
`assert` 方法使用 `asserts condition` 类型,这是 TypeScript 的断言函数:
|
|
105
|
-
- 如果条件为真,函数正常返回
|
|
106
|
-
- 如果条件为假,函数抛出异常
|
|
107
|
-
- TypeScript 会根据条件自动进行类型收窄
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
// 类型定义
|
|
111
|
-
static assert(
|
|
112
|
-
condition: any,
|
|
113
|
-
code: string,
|
|
114
|
-
paramsOrLocale?: Record<string, any> | string,
|
|
115
|
-
statusCode?: number,
|
|
116
|
-
locale?: string
|
|
117
|
-
): asserts condition;
|
|
118
|
-
|
|
119
|
-
// 使用示例
|
|
120
|
-
function test(account: any) {
|
|
121
|
-
I18nError.assert(account, 'account.notFound');
|
|
122
|
-
// ✅ TypeScript 知道 account 一定存在(非 falsy)
|
|
123
|
-
console.log(account.id);
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
## ✅ 验证结果
|
|
130
|
-
|
|
131
|
-
### 测试覆盖
|
|
132
|
-
- ✅ 所有 782 个测试通过
|
|
133
|
-
- ✅ 测试耗时: ~1.5 秒
|
|
134
|
-
- ✅ 特别验证了 `dsl.error` 相关的 3 个测试
|
|
135
|
-
|
|
136
|
-
### TypeScript 编译检查
|
|
137
|
-
- ✅ 运行 `npx tsc --noEmit`,无编译错误
|
|
138
|
-
- ✅ 控制流分析正确推断变量类型
|
|
139
|
-
- ✅ 在条件判断后正确收窄类型范围
|
|
140
|
-
|
|
141
|
-
### 兼容性
|
|
142
|
-
- ✅ **向后兼容**: 不影响现有功能
|
|
143
|
-
- ✅ **运行时行为**: 第二个 throw 永远不会执行
|
|
144
|
-
- ✅ **性能影响**: 无(代码不可达)
|
|
145
|
-
- ✅ **测试覆盖**: 所有现有测试通过
|
|
146
|
-
|
|
147
|
-
---
|
|
148
|
-
|
|
149
|
-
## 📦 影响范围
|
|
150
|
-
|
|
151
|
-
### 修改文件
|
|
152
|
-
- `lib/errors/I18nError.js` (L211)
|
|
153
|
-
- `index.js` (L94-97)
|
|
154
|
-
|
|
155
|
-
### 未修改文件
|
|
156
|
-
- `index.d.ts` - 类型定义已经正确(返回类型为 `never`)
|
|
157
|
-
- 所有测试文件 - 无需修改
|
|
158
|
-
- 所有文档文件 - 无需更新
|
|
159
|
-
|
|
160
|
-
---
|
|
161
|
-
|
|
162
|
-
## 🔄 升级指南
|
|
163
|
-
|
|
164
|
-
### 从 v1.1.8 升级到 v1.2.0
|
|
165
|
-
|
|
166
|
-
**无需任何代码修改**!本次修复完全向后兼容。
|
|
167
|
-
|
|
168
|
-
```bash
|
|
169
|
-
# 升级命令
|
|
170
|
-
npm update schema-dsl
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
**验证升级**:
|
|
174
|
-
```javascript
|
|
175
|
-
const { dsl } = require('schema-dsl');
|
|
176
|
-
|
|
177
|
-
// 测试 throw 方法
|
|
178
|
-
try {
|
|
179
|
-
dsl.error.throw('test.error');
|
|
180
|
-
} catch (error) {
|
|
181
|
-
console.log('✅ throw 方法工作正常');
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// 测试 assert 方法
|
|
185
|
-
try {
|
|
186
|
-
dsl.error.assert(false, 'test.error');
|
|
187
|
-
} catch (error) {
|
|
188
|
-
console.log('✅ assert 方法工作正常');
|
|
189
|
-
}
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
---
|
|
193
|
-
|
|
194
|
-
## 📚 相关文档
|
|
195
|
-
|
|
196
|
-
- [TypeScript 使用指南](../docs/typescript-guide.md)
|
|
197
|
-
- [错误处理文档](../docs/error-handling.md)
|
|
198
|
-
- [API 参考文档](../docs/api-reference.md)
|
|
199
|
-
|
|
200
|
-
---
|
|
201
|
-
|
|
202
|
-
## 🙏 致谢
|
|
203
|
-
|
|
204
|
-
感谢社区用户反馈此类型问题,帮助我们改进 TypeScript 类型定义的准确性。
|
|
205
|
-
|
|
206
|
-
---
|
|
207
|
-
|
|
208
|
-
## 📊 版本对比
|
|
209
|
-
|
|
210
|
-
| 项目 | v1.1.8 | v1.2.0 |
|
|
211
|
-
|------|--------|--------|
|
|
212
|
-
| TypeScript never 类型 | ⚠️ 可能报错 | ✅ 正确 |
|
|
213
|
-
| 测试通过率 | 100% (782/782) | 100% (782/782) |
|
|
214
|
-
| 破坏性变更 | 无 | 无 |
|
|
215
|
-
| 需要代码修改 | 否 | 否 |
|
|
216
|
-
|
|
217
|
-
---
|
|
218
|
-
|
|
219
|
-
**完整变更历史**: [CHANGELOG.md](../CHANGELOG.md)
|
|
220
|
-
**下一个版本规划**: 待定
|