@pilllesss/yorn 1.0.182 → 1.0.183
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/README.md +1 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/skills/code-review/LICENSE +21 -0
- package/dist/skills/code-review/SKILL.md +233 -0
- package/dist/skills/code-review/assets/pr-review-template.md +137 -0
- package/dist/skills/code-review/assets/review-checklist.md +123 -0
- package/dist/skills/code-review/reference/angular.md +768 -0
- package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
- package/dist/skills/code-review/reference/c.md +890 -0
- package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
- package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
- package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
- package/dist/skills/code-review/reference/cpp.md +893 -0
- package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
- package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
- package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
- package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
- package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
- package/dist/skills/code-review/reference/csharp.md +525 -0
- package/dist/skills/code-review/reference/css-less-sass.md +661 -0
- package/dist/skills/code-review/reference/dart.md +670 -0
- package/dist/skills/code-review/reference/django.md +985 -0
- package/dist/skills/code-review/reference/fastapi.md +580 -0
- package/dist/skills/code-review/reference/go.md +993 -0
- package/dist/skills/code-review/reference/java.md +409 -0
- package/dist/skills/code-review/reference/java8.md +586 -0
- package/dist/skills/code-review/reference/kotlin.md +1018 -0
- package/dist/skills/code-review/reference/nestjs.md +593 -0
- package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
- package/dist/skills/code-review/reference/php.md +684 -0
- package/dist/skills/code-review/reference/python.md +1073 -0
- package/dist/skills/code-review/reference/qt.md +757 -0
- package/dist/skills/code-review/reference/react.md +871 -0
- package/dist/skills/code-review/reference/ruby.md +964 -0
- package/dist/skills/code-review/reference/rust.md +846 -0
- package/dist/skills/code-review/reference/security-review-guide.md +494 -0
- package/dist/skills/code-review/reference/svelte.md +1064 -0
- package/dist/skills/code-review/reference/swift.md +936 -0
- package/dist/skills/code-review/reference/typescript.md +1016 -0
- package/dist/skills/code-review/reference/vue.md +924 -0
- package/dist/skills/code-review/reference/zig.md +440 -0
- package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
- package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
- package/dist/yorn.cjs +628 -628
- package/package.json +2 -2
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
# TypeScript/JavaScript Code Review Guide
|
|
2
|
+
|
|
3
|
+
> TypeScript 代码审查指南,覆盖类型系统、泛型、条件类型、strict 模式、async/await 模式等核心主题。
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
- [类型安全基础](#类型安全基础)
|
|
8
|
+
- [泛型模式](#泛型模式)
|
|
9
|
+
- [高级类型](#高级类型)
|
|
10
|
+
- [Strict 模式配置](#strict-模式配置)
|
|
11
|
+
- [异步处理](#异步处理)
|
|
12
|
+
- [不可变性](#不可变性)
|
|
13
|
+
- [ESLint 规则](#eslint-规则)
|
|
14
|
+
- [测试](#测试)
|
|
15
|
+
- [模块解析](#模块解析)
|
|
16
|
+
- [TS 4.9+ / 5.x 新特性](#ts-49--5x-新特性)
|
|
17
|
+
- [Review Checklist](#review-checklist)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 类型安全基础
|
|
22
|
+
|
|
23
|
+
### 避免使用 any
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// ❌ Using any defeats type safety
|
|
27
|
+
function processData(data: any) {
|
|
28
|
+
return data.value; // 无类型检查,运行时可能崩溃
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ✅ Use proper types
|
|
32
|
+
interface DataPayload {
|
|
33
|
+
value: string;
|
|
34
|
+
}
|
|
35
|
+
function processData(data: DataPayload) {
|
|
36
|
+
return data.value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ✅ 未知类型用 unknown + 类型守卫
|
|
40
|
+
function processUnknown(data: unknown) {
|
|
41
|
+
if (typeof data === 'object' && data !== null && 'value' in data) {
|
|
42
|
+
return (data as { value: string }).value;
|
|
43
|
+
}
|
|
44
|
+
throw new Error('Invalid data');
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 类型收窄
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// ❌ 不安全的类型断言
|
|
52
|
+
function getLength(value: string | string[]) {
|
|
53
|
+
return (value as string[]).length; // 如果是 string 会出错
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ✅ 使用类型守卫
|
|
57
|
+
function getLength(value: string | string[]): number {
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
return value.length;
|
|
60
|
+
}
|
|
61
|
+
return value.length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ✅ 使用 in 操作符
|
|
65
|
+
interface Dog { bark(): void }
|
|
66
|
+
interface Cat { meow(): void }
|
|
67
|
+
|
|
68
|
+
function speak(animal: Dog | Cat) {
|
|
69
|
+
if ('bark' in animal) {
|
|
70
|
+
animal.bark();
|
|
71
|
+
} else {
|
|
72
|
+
animal.meow();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 字面量类型与 as const
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// ❌ 类型过于宽泛
|
|
81
|
+
const config = {
|
|
82
|
+
endpoint: '/api',
|
|
83
|
+
method: 'GET' // 类型是 string
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// ✅ 使用 as const 获得字面量类型
|
|
87
|
+
const config = {
|
|
88
|
+
endpoint: '/api',
|
|
89
|
+
method: 'GET'
|
|
90
|
+
} as const; // method 类型是 'GET'
|
|
91
|
+
|
|
92
|
+
// ✅ 用于函数参数
|
|
93
|
+
function request(method: 'GET' | 'POST', url: string) { ... }
|
|
94
|
+
request(config.method, config.endpoint); // 正确!
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 泛型模式
|
|
100
|
+
|
|
101
|
+
### 基础泛型
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// ❌ 重复代码
|
|
105
|
+
function getFirstString(arr: string[]): string | undefined {
|
|
106
|
+
return arr[0];
|
|
107
|
+
}
|
|
108
|
+
function getFirstNumber(arr: number[]): number | undefined {
|
|
109
|
+
return arr[0];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ✅ 使用泛型
|
|
113
|
+
function getFirst<T>(arr: T[]): T | undefined {
|
|
114
|
+
return arr[0];
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 泛型约束
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// ❌ 泛型没有约束,无法访问属性
|
|
122
|
+
function getProperty<T>(obj: T, key: string) {
|
|
123
|
+
return obj[key]; // Error: 无法索引
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ✅ 使用 keyof 约束
|
|
127
|
+
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
|
|
128
|
+
return obj[key];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const user = { name: 'Alice', age: 30 };
|
|
132
|
+
getProperty(user, 'name'); // 返回类型是 string
|
|
133
|
+
getProperty(user, 'age'); // 返回类型是 number
|
|
134
|
+
getProperty(user, 'foo'); // Error: 'foo' 不在 keyof User
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 泛型默认值
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// ✅ 提供合理的默认类型
|
|
141
|
+
interface ApiResponse<T = unknown> {
|
|
142
|
+
data: T;
|
|
143
|
+
status: number;
|
|
144
|
+
message: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 可以不指定泛型参数
|
|
148
|
+
const response: ApiResponse = { data: null, status: 200, message: 'OK' };
|
|
149
|
+
// 也可以指定
|
|
150
|
+
const userResponse: ApiResponse<User> = { ... };
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 常见泛型工具类型
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// ✅ 善用内置工具类型
|
|
157
|
+
interface User {
|
|
158
|
+
id: number;
|
|
159
|
+
name: string;
|
|
160
|
+
email: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
type PartialUser = Partial<User>; // 所有属性可选
|
|
164
|
+
type RequiredUser = Required<User>; // 所有属性必需
|
|
165
|
+
type ReadonlyUser = Readonly<User>; // 所有属性只读
|
|
166
|
+
type UserKeys = keyof User; // 'id' | 'name' | 'email'
|
|
167
|
+
type NameOnly = Pick<User, 'name'>; // { name: string }
|
|
168
|
+
type WithoutId = Omit<User, 'id'>; // { name: string; email: string }
|
|
169
|
+
type UserRecord = Record<string, User>; // { [key: string]: User }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 高级类型
|
|
175
|
+
|
|
176
|
+
### 条件类型
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// ✅ 根据输入类型返回不同类型
|
|
180
|
+
type IsString<T> = T extends string ? true : false;
|
|
181
|
+
|
|
182
|
+
type A = IsString<string>; // true
|
|
183
|
+
type B = IsString<number>; // false
|
|
184
|
+
|
|
185
|
+
// ✅ 提取数组元素类型
|
|
186
|
+
type ElementType<T> = T extends (infer U)[] ? U : never;
|
|
187
|
+
|
|
188
|
+
type Elem = ElementType<string[]>; // string
|
|
189
|
+
|
|
190
|
+
// ✅ 提取函数返回类型(内置 ReturnType)
|
|
191
|
+
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 映射类型
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// ✅ 转换对象类型的所有属性
|
|
198
|
+
type Nullable<T> = {
|
|
199
|
+
[K in keyof T]: T[K] | null;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
interface User {
|
|
203
|
+
name: string;
|
|
204
|
+
age: number;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
type NullableUser = Nullable<User>;
|
|
208
|
+
// { name: string | null; age: number | null }
|
|
209
|
+
|
|
210
|
+
// ✅ 添加前缀
|
|
211
|
+
type Getters<T> = {
|
|
212
|
+
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
type UserGetters = Getters<User>;
|
|
216
|
+
// { getName: () => string; getAge: () => number }
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 模板字面量类型
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// ✅ 类型安全的事件名称
|
|
223
|
+
type EventName = 'click' | 'focus' | 'blur';
|
|
224
|
+
type HandlerName = `on${Capitalize<EventName>}`;
|
|
225
|
+
// 'onClick' | 'onFocus' | 'onBlur'
|
|
226
|
+
|
|
227
|
+
// ✅ API 路由类型
|
|
228
|
+
type ApiRoute = `/api/${string}`;
|
|
229
|
+
const route: ApiRoute = '/api/users'; // OK
|
|
230
|
+
const badRoute: ApiRoute = '/users'; // Error
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Discriminated Unions
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// ✅ 使用判别属性实现类型安全
|
|
237
|
+
type Result<T, E> =
|
|
238
|
+
| { success: true; data: T }
|
|
239
|
+
| { success: false; error: E };
|
|
240
|
+
|
|
241
|
+
function handleResult(result: Result<User, Error>) {
|
|
242
|
+
if (result.success) {
|
|
243
|
+
console.log(result.data.name); // TypeScript 知道 data 存在
|
|
244
|
+
} else {
|
|
245
|
+
console.log(result.error.message); // TypeScript 知道 error 存在
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ✅ Redux Action 模式
|
|
250
|
+
type Action =
|
|
251
|
+
| { type: 'INCREMENT'; payload: number }
|
|
252
|
+
| { type: 'DECREMENT'; payload: number }
|
|
253
|
+
| { type: 'RESET' };
|
|
254
|
+
|
|
255
|
+
function reducer(state: number, action: Action): number {
|
|
256
|
+
switch (action.type) {
|
|
257
|
+
case 'INCREMENT':
|
|
258
|
+
return state + action.payload; // payload 类型已知
|
|
259
|
+
case 'DECREMENT':
|
|
260
|
+
return state - action.payload;
|
|
261
|
+
case 'RESET':
|
|
262
|
+
return 0; // 这里没有 payload
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Strict 模式配置
|
|
270
|
+
|
|
271
|
+
### 推荐的 tsconfig.json
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
{
|
|
275
|
+
"compilerOptions": {
|
|
276
|
+
// ✅ 必须开启的 strict 选项
|
|
277
|
+
"strict": true,
|
|
278
|
+
"noImplicitAny": true,
|
|
279
|
+
"strictNullChecks": true,
|
|
280
|
+
"strictFunctionTypes": true,
|
|
281
|
+
"strictBindCallApply": true,
|
|
282
|
+
"strictPropertyInitialization": true,
|
|
283
|
+
"noImplicitThis": true,
|
|
284
|
+
"useUnknownInCatchVariables": true,
|
|
285
|
+
|
|
286
|
+
// ✅ 额外推荐选项
|
|
287
|
+
"noUncheckedIndexedAccess": true,
|
|
288
|
+
"noImplicitReturns": true,
|
|
289
|
+
"noFallthroughCasesInSwitch": true,
|
|
290
|
+
"exactOptionalPropertyTypes": true,
|
|
291
|
+
"noPropertyAccessFromIndexSignature": true
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### noUncheckedIndexedAccess 的影响
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
// tsconfig: "noUncheckedIndexedAccess": true
|
|
300
|
+
|
|
301
|
+
const arr = [1, 2, 3];
|
|
302
|
+
const first = arr[0]; // 类型是 number | undefined
|
|
303
|
+
|
|
304
|
+
// ❌ 直接使用可能出错
|
|
305
|
+
console.log(first.toFixed(2)); // Error: 可能是 undefined
|
|
306
|
+
|
|
307
|
+
// ✅ 先检查
|
|
308
|
+
if (first !== undefined) {
|
|
309
|
+
console.log(first.toFixed(2));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ✅ 或使用非空断言(确定时)
|
|
313
|
+
console.log(arr[0]!.toFixed(2));
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## 异步处理
|
|
319
|
+
|
|
320
|
+
### Promise 错误处理
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// ❌ Not handling async errors
|
|
324
|
+
async function fetchUser(id: string) {
|
|
325
|
+
const response = await fetch(`/api/users/${id}`);
|
|
326
|
+
return response.json(); // 网络错误未处理
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ✅ Handle errors properly
|
|
330
|
+
async function fetchUser(id: string): Promise<User> {
|
|
331
|
+
try {
|
|
332
|
+
const response = await fetch(`/api/users/${id}`);
|
|
333
|
+
if (!response.ok) {
|
|
334
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
335
|
+
}
|
|
336
|
+
return await response.json();
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (error instanceof Error) {
|
|
339
|
+
throw new Error(`Failed to fetch user: ${error.message}`);
|
|
340
|
+
}
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Promise.all vs Promise.allSettled
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
// ❌ Promise.all 一个失败全部失败
|
|
350
|
+
async function fetchAllUsers(ids: string[]) {
|
|
351
|
+
const users = await Promise.all(ids.map(fetchUser));
|
|
352
|
+
return users; // 一个失败就全部失败
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ✅ Promise.allSettled 获取所有结果
|
|
356
|
+
async function fetchAllUsers(ids: string[]) {
|
|
357
|
+
const results = await Promise.allSettled(ids.map(fetchUser));
|
|
358
|
+
|
|
359
|
+
const users: User[] = [];
|
|
360
|
+
const errors: Error[] = [];
|
|
361
|
+
|
|
362
|
+
for (const result of results) {
|
|
363
|
+
if (result.status === 'fulfilled') {
|
|
364
|
+
users.push(result.value);
|
|
365
|
+
} else {
|
|
366
|
+
errors.push(result.reason);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return { users, errors };
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### 竞态条件处理
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
// ❌ 竞态条件:旧请求可能覆盖新请求
|
|
378
|
+
function useSearch() {
|
|
379
|
+
const [query, setQuery] = useState('');
|
|
380
|
+
const [results, setResults] = useState([]);
|
|
381
|
+
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
fetch(`/api/search?q=${query}`)
|
|
384
|
+
.then(r => r.json())
|
|
385
|
+
.then(setResults); // 旧请求可能后返回!
|
|
386
|
+
}, [query]);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ✅ 使用 AbortController
|
|
390
|
+
function useSearch() {
|
|
391
|
+
const [query, setQuery] = useState('');
|
|
392
|
+
const [results, setResults] = useState([]);
|
|
393
|
+
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
const controller = new AbortController();
|
|
396
|
+
|
|
397
|
+
fetch(`/api/search?q=${query}`, { signal: controller.signal })
|
|
398
|
+
.then(r => r.json())
|
|
399
|
+
.then(setResults)
|
|
400
|
+
.catch(e => {
|
|
401
|
+
if (e.name !== 'AbortError') throw e;
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
return () => controller.abort();
|
|
405
|
+
}, [query]);
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## 不可变性
|
|
412
|
+
|
|
413
|
+
### Readonly 与 ReadonlyArray
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
// ❌ 可变参数可能被意外修改
|
|
417
|
+
function processUsers(users: User[]) {
|
|
418
|
+
users.sort((a, b) => a.name.localeCompare(b.name)); // 修改了原数组!
|
|
419
|
+
return users;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ✅ 使用 readonly 防止修改
|
|
423
|
+
function processUsers(users: readonly User[]): User[] {
|
|
424
|
+
return [...users].sort((a, b) => a.name.localeCompare(b.name));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ✅ 深度只读
|
|
428
|
+
type DeepReadonly<T> = {
|
|
429
|
+
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
|
|
430
|
+
};
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### 不变式函数参数
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
// ✅ 使用 as const 和 readonly 保护数据
|
|
437
|
+
function createConfig<T extends readonly string[]>(routes: T) {
|
|
438
|
+
return routes;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const routes = createConfig(['home', 'about', 'contact'] as const);
|
|
442
|
+
// 类型是 readonly ['home', 'about', 'contact']
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## ESLint 规则
|
|
448
|
+
|
|
449
|
+
### 推荐的 @typescript-eslint 规则
|
|
450
|
+
|
|
451
|
+
```javascript
|
|
452
|
+
// eslint.config.js(flat config,typescript-eslint v8)
|
|
453
|
+
import eslint from '@eslint/js';
|
|
454
|
+
import tseslint from 'typescript-eslint';
|
|
455
|
+
|
|
456
|
+
export default tseslint.config(
|
|
457
|
+
eslint.configs.recommended,
|
|
458
|
+
// 需要类型信息的规则集,对应旧的 recommended-requiring-type-checking
|
|
459
|
+
tseslint.configs.recommendedTypeChecked,
|
|
460
|
+
tseslint.configs.strictTypeChecked,
|
|
461
|
+
{
|
|
462
|
+
languageOptions: {
|
|
463
|
+
parserOptions: {
|
|
464
|
+
// 让带类型的规则自动找到对应 tsconfig
|
|
465
|
+
projectService: true,
|
|
466
|
+
tsconfigRootDir: import.meta.dirname,
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
rules: {
|
|
470
|
+
// ✅ 类型安全
|
|
471
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
472
|
+
'@typescript-eslint/no-unsafe-assignment': 'error',
|
|
473
|
+
'@typescript-eslint/no-unsafe-member-access': 'error',
|
|
474
|
+
'@typescript-eslint/no-unsafe-call': 'error',
|
|
475
|
+
'@typescript-eslint/no-unsafe-return': 'error',
|
|
476
|
+
|
|
477
|
+
// ✅ 最佳实践
|
|
478
|
+
'@typescript-eslint/explicit-function-return-type': 'warn',
|
|
479
|
+
'@typescript-eslint/no-floating-promises': 'error',
|
|
480
|
+
'@typescript-eslint/await-thenable': 'error',
|
|
481
|
+
'@typescript-eslint/no-misused-promises': 'error',
|
|
482
|
+
|
|
483
|
+
// ✅ 代码风格
|
|
484
|
+
'@typescript-eslint/consistent-type-imports': 'error',
|
|
485
|
+
'@typescript-eslint/prefer-nullish-coalescing': 'error',
|
|
486
|
+
'@typescript-eslint/prefer-optional-chain': 'error',
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
);
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### 常见 ESLint 错误修复
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
// ❌ no-floating-promises: Promise 必须被处理
|
|
496
|
+
async function save() { ... }
|
|
497
|
+
save(); // Error: 未处理的 Promise
|
|
498
|
+
|
|
499
|
+
// ✅ 显式处理
|
|
500
|
+
await save();
|
|
501
|
+
// 或
|
|
502
|
+
save().catch(console.error);
|
|
503
|
+
// 或明确忽略
|
|
504
|
+
void save();
|
|
505
|
+
|
|
506
|
+
// ❌ no-misused-promises: 不能在非 async 位置使用 Promise
|
|
507
|
+
const items = [1, 2, 3];
|
|
508
|
+
items.forEach(async (item) => { // Error!
|
|
509
|
+
await processItem(item);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// ✅ 使用 for...of
|
|
513
|
+
for (const item of items) {
|
|
514
|
+
await processItem(item);
|
|
515
|
+
}
|
|
516
|
+
// 或 Promise.all
|
|
517
|
+
await Promise.all(items.map(processItem));
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## 测试
|
|
525
|
+
|
|
526
|
+
### Vitest vs Jest 选择
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
// ✅ 新项目推荐 Vitest(与 Vite 生态集成,原生 ESM 支持)
|
|
530
|
+
// vitest.config.ts
|
|
531
|
+
import { defineConfig } from 'vitest/config';
|
|
532
|
+
|
|
533
|
+
export default defineConfig({
|
|
534
|
+
test: {
|
|
535
|
+
globals: true,
|
|
536
|
+
environment: 'node',
|
|
537
|
+
include: ['src/**/*.test.ts'],
|
|
538
|
+
coverage: {
|
|
539
|
+
provider: 'v8',
|
|
540
|
+
reporter: ['text', 'lcov'],
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// ✅ 已有 Jest 项目可保持,注意配置差异
|
|
546
|
+
// jest.config.ts
|
|
547
|
+
import type { Config } from 'jest';
|
|
548
|
+
|
|
549
|
+
const config: Config = {
|
|
550
|
+
preset: 'ts-jest',
|
|
551
|
+
testEnvironment: 'node',
|
|
552
|
+
moduleNameMapper: {
|
|
553
|
+
'^@/(.*)$': '<rootDir>/src/$1',
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
export default config;
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### 类型测试(tsd / expect-type)
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
// ✅ 使用 expect-type 验证类型推断
|
|
563
|
+
import { expectTypeOf } from 'vitest';
|
|
564
|
+
|
|
565
|
+
function getFirst<T>(arr: T[]): T | undefined {
|
|
566
|
+
return arr[0];
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
it('should infer correct return type', () => {
|
|
570
|
+
const result = getFirst([1, 2, 3]);
|
|
571
|
+
expectTypeOf(result).toEqualTypeOf<number | undefined>();
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// ✅ 使用 expect-type 验证函数签名
|
|
575
|
+
const fn = (a: string, b: number) => a.repeat(b);
|
|
576
|
+
expectTypeOf(fn).parameters.toEqualTypeOf<[string, number]>();
|
|
577
|
+
expectTypeOf(fn).returns.toBeString();
|
|
578
|
+
|
|
579
|
+
// ❌ 类型错误会在编译时被捕获
|
|
580
|
+
const result = getFirst(['a', 'b']);
|
|
581
|
+
// @ts-expect-error: 类型不匹配
|
|
582
|
+
expectTypeOf(result).toEqualTypeOf<number>();
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### Snapshot 测试最佳实践
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
// ✅ Snapshot 适合:稳定的输出结构、配置对象、错误消息
|
|
589
|
+
it('should match serialized config', () => {
|
|
590
|
+
const config = createAppConfig();
|
|
591
|
+
expect(config).toMatchSnapshot();
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// ❌ 避免:大对象、动态数据、随机值
|
|
595
|
+
it('should not snapshot large payloads', () => {
|
|
596
|
+
const hugePayload = { users: generateRandomUsers(1000) };
|
|
597
|
+
// 太长的 snapshot 难以审查,变更时不知道意图
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// ✅ 使用 inline snapshot 处理小片段
|
|
601
|
+
it('should generate correct error message', () => {
|
|
602
|
+
expect(formatError('INVALID_INPUT')).toMatchInlineSnapshot(
|
|
603
|
+
`"Error: Invalid input provided"`
|
|
604
|
+
);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// ✅ 使用 snapshot 属性匹配器处理动态值
|
|
608
|
+
it('should match user with generated id', () => {
|
|
609
|
+
expect(createUser('Alice')).toMatchSnapshot({
|
|
610
|
+
id: expect.any(String),
|
|
611
|
+
createdAt: expect.any(Date),
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### Mock 策略
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
// ✅ Vitest: vi.mock 自动 hoist
|
|
620
|
+
import { vi, describe, it, expect } from 'vitest';
|
|
621
|
+
|
|
622
|
+
vi.mock('./api', () => ({
|
|
623
|
+
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
|
|
624
|
+
}));
|
|
625
|
+
|
|
626
|
+
it('should display user', async () => {
|
|
627
|
+
const { fetchUser } = await import('./api');
|
|
628
|
+
const user = await fetchUser('1');
|
|
629
|
+
expect(user.name).toBe('Alice');
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// ✅ Jest: jest.mock 同样自动 hoist
|
|
633
|
+
jest.mock('./database', () => ({
|
|
634
|
+
query: jest.fn().mockResolvedValue([{ id: 1 }]),
|
|
635
|
+
}));
|
|
636
|
+
|
|
637
|
+
// ❌ 避免部分 Mock——测试的是 Mock 而非真实行为
|
|
638
|
+
jest.mock('./utils', () => ({
|
|
639
|
+
...jest.requireActual('./utils'),
|
|
640
|
+
calculateTotal: jest.fn(), // 其他函数是真实的,这个是假的
|
|
641
|
+
}));
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### 测试辅助工具
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
// ✅ 使用 testing-library 进行 DOM 测试
|
|
648
|
+
import { render, screen } from '@testing-library/react';
|
|
649
|
+
import userEvent from '@testing-library/user-event';
|
|
650
|
+
|
|
651
|
+
it('should submit form', async () => {
|
|
652
|
+
render(<LoginForm />);
|
|
653
|
+
await userEvent.type(screen.getByLabelText('Email'), 'alice@example.com');
|
|
654
|
+
await userEvent.click(screen.getByRole('button', { name: 'Submit' }));
|
|
655
|
+
expect(screen.getByText('Welcome, Alice!')).toBeInTheDocument();
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// ✅ 使用 MSW 进行 API mock(Mock Service Worker)
|
|
659
|
+
import { http, HttpResponse } from 'msw';
|
|
660
|
+
import { setupServer } from 'msw/node';
|
|
661
|
+
|
|
662
|
+
const server = setupServer(
|
|
663
|
+
http.get('/api/users/:id', ({ params }) => {
|
|
664
|
+
return HttpResponse.json({ id: params.id, name: 'Alice' });
|
|
665
|
+
})
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
beforeAll(() => server.listen());
|
|
669
|
+
afterEach(() => server.resetHandlers());
|
|
670
|
+
afterAll(() => server.close());
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## 模块解析
|
|
676
|
+
|
|
677
|
+
### ESM vs CJS 差异和陷阱
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
// ❌ CJS 风格在 ESM 中不可用
|
|
681
|
+
// package.json: "type": "module"
|
|
682
|
+
const fs = require('fs'); // Error: require is not defined
|
|
683
|
+
module.exports = { foo: 'bar' }; // Error: module is not defined
|
|
684
|
+
|
|
685
|
+
// ✅ ESM 正确写法
|
|
686
|
+
import fs from 'node:fs';
|
|
687
|
+
export const foo = 'bar';
|
|
688
|
+
|
|
689
|
+
// ✅ 在 ESM 中获取 __dirname
|
|
690
|
+
import { fileURLToPath } from 'node:url';
|
|
691
|
+
import { dirname } from 'node:path';
|
|
692
|
+
|
|
693
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
694
|
+
const __dirname = dirname(__filename);
|
|
695
|
+
|
|
696
|
+
// ❌ ESM 中动态 require
|
|
697
|
+
const moduleName = 'lodash';
|
|
698
|
+
const _ = require(moduleName); // Error!
|
|
699
|
+
|
|
700
|
+
// ✅ ESM 动态 import
|
|
701
|
+
const _ = await import(moduleName);
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### tsconfig paths 与 path aliases
|
|
705
|
+
|
|
706
|
+
```json
|
|
707
|
+
// tsconfig.json
|
|
708
|
+
{
|
|
709
|
+
"compilerOptions": {
|
|
710
|
+
"baseUrl": ".",
|
|
711
|
+
"paths": {
|
|
712
|
+
"@/*": ["./src/*"],
|
|
713
|
+
"@components/*": ["./src/components/*"],
|
|
714
|
+
"@utils/*": ["./src/utils/*"]
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
```typescript
|
|
721
|
+
// ✅ 使用别名前
|
|
722
|
+
import { Button } from '../../components/ui/Button';
|
|
723
|
+
import { formatDate } from '../../../utils/date';
|
|
724
|
+
|
|
725
|
+
// ✅ 使用别名后——清晰且不易因文件移动而断裂
|
|
726
|
+
import { Button } from '@components/ui/Button';
|
|
727
|
+
import { formatDate } from '@utils/date';
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
```typescript
|
|
731
|
+
// ⚠️ tsconfig paths 只影响 TS 编译,不影响运行时
|
|
732
|
+
// 需要配合打包工具(Vite、webpack)或 tsx 的别名解析
|
|
733
|
+
|
|
734
|
+
// vite.config.ts
|
|
735
|
+
import { resolve } from 'node:path';
|
|
736
|
+
|
|
737
|
+
export default defineConfig({
|
|
738
|
+
resolve: {
|
|
739
|
+
alias: {
|
|
740
|
+
'@': resolve(__dirname, 'src'),
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// ⚠️ 发布 npm 包时,tsconfig paths 不会自动解析
|
|
746
|
+
// 需要 tsc-alias 或 tsconfig-paths 处理
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### package.json exports field
|
|
750
|
+
|
|
751
|
+
```json
|
|
752
|
+
// package.json
|
|
753
|
+
{
|
|
754
|
+
"name": "my-library",
|
|
755
|
+
"exports": {
|
|
756
|
+
".": {
|
|
757
|
+
"import": "./dist/index.mjs",
|
|
758
|
+
"require": "./dist/index.cjs",
|
|
759
|
+
"types": "./dist/index.d.ts"
|
|
760
|
+
},
|
|
761
|
+
"./utils": {
|
|
762
|
+
"import": "./dist/utils.mjs",
|
|
763
|
+
"require": "./dist/utils.cjs",
|
|
764
|
+
"types": "./dist/utils.d.ts"
|
|
765
|
+
},
|
|
766
|
+
"./*": "./dist/*"
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
```typescript
|
|
772
|
+
// ✅ 消费者使用
|
|
773
|
+
import { foo } from 'my-library'; // 解析到 "." 条件
|
|
774
|
+
import { bar } from 'my-library/utils'; // 解析到 "./utils" 条件
|
|
775
|
+
|
|
776
|
+
// ❌ 没有 exports 映射的路径无法访问
|
|
777
|
+
import { secret } from 'my-library/internal'; // Error!
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
### 动态 import() 和代码分割
|
|
781
|
+
|
|
782
|
+
```typescript
|
|
783
|
+
// ✅ 条件加载模块
|
|
784
|
+
async function loadChartLibrary() {
|
|
785
|
+
if (typeof window === 'undefined') return null; // SSR 跳过
|
|
786
|
+
const { Chart } = await import('chart.js');
|
|
787
|
+
return Chart;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// ✅ React 懒加载组件
|
|
791
|
+
const AdminPanel = lazy(() => import('./AdminPanel'));
|
|
792
|
+
// 配合 Suspense 使用
|
|
793
|
+
<Suspense fallback={<Loading />}>
|
|
794
|
+
<AdminPanel />
|
|
795
|
+
</Suspense>
|
|
796
|
+
|
|
797
|
+
// ✅ 带错误处理
|
|
798
|
+
const AdminPanel = lazy(() =>
|
|
799
|
+
import('./AdminPanel').catch(() => ({
|
|
800
|
+
default: () => <ErrorFallback />,
|
|
801
|
+
}))
|
|
802
|
+
);
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
---
|
|
806
|
+
|
|
807
|
+
## TS 4.9+ / 5.x 新特性
|
|
808
|
+
|
|
809
|
+
### satisfies 关键字(TS 4.9+)
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
// ❌ 没有 satisfies:类型太宽泛
|
|
813
|
+
const palette = {
|
|
814
|
+
red: '#ff0000',
|
|
815
|
+
green: '#00ff00',
|
|
816
|
+
blue: '#0000ff',
|
|
817
|
+
};
|
|
818
|
+
// palette.red 类型是 string,丢失了 '#ff0000' 的精确值
|
|
819
|
+
|
|
820
|
+
// ✅ satisfies 保留字面量类型,同时验证结构
|
|
821
|
+
const palette = {
|
|
822
|
+
red: '#ff0000',
|
|
823
|
+
green: '#00ff00',
|
|
824
|
+
blue: '#0000ff',
|
|
825
|
+
} satisfies Record<string, `#${string}`>;
|
|
826
|
+
|
|
827
|
+
// palette.red 类型是 '#ff0000'(不是 string)
|
|
828
|
+
// 但添加新属性时仍会验证格式
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
```typescript
|
|
832
|
+
// ✅ satisfies 用于验证对象符合接口
|
|
833
|
+
interface UserConfig {
|
|
834
|
+
theme: 'light' | 'dark';
|
|
835
|
+
locale: string;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const config = {
|
|
839
|
+
theme: 'dark',
|
|
840
|
+
locale: 'en-US',
|
|
841
|
+
} satisfies UserConfig;
|
|
842
|
+
// config.theme 类型是 'dark'(不是 'light' | 'dark')
|
|
843
|
+
// 所有属性都通过 satisfies 类型检查
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
### const 类型参数(TS 5.0+)
|
|
847
|
+
|
|
848
|
+
```typescript
|
|
849
|
+
// ❌ 之前:需要 as const 断言
|
|
850
|
+
function getRoutes<T extends readonly string[]>(routes: T) {
|
|
851
|
+
return routes;
|
|
852
|
+
}
|
|
853
|
+
const routes = getRoutes(['home', 'about'] as const);
|
|
854
|
+
|
|
855
|
+
// ✅ TS 5.0+:const 类型参数
|
|
856
|
+
function getRoutes<const T extends readonly string[]>(routes: T) {
|
|
857
|
+
return routes;
|
|
858
|
+
}
|
|
859
|
+
const routes = getRoutes(['home', 'about']);
|
|
860
|
+
// routes 类型是 readonly ['home', 'about']
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
```typescript
|
|
864
|
+
// ✅ 真实场景:类型安全的配置对象
|
|
865
|
+
declare function createConfig<const T extends Record<string, unknown>>(
|
|
866
|
+
config: T
|
|
867
|
+
): T;
|
|
868
|
+
|
|
869
|
+
const config = createConfig({
|
|
870
|
+
api: { url: 'https://api.example.com', version: 2 },
|
|
871
|
+
features: { newDashboard: true },
|
|
872
|
+
});
|
|
873
|
+
// config.api.url 类型是 'https://api.example.com'(字面量)
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
### 装饰器(Stage 3 Decorators, TS 5.0+)
|
|
877
|
+
|
|
878
|
+
```typescript
|
|
879
|
+
// ✅ Stage 3 装饰器(TS 5.0+,experimentalDecorators 不再需要)
|
|
880
|
+
function logged<This, Args extends unknown[], Return>(
|
|
881
|
+
target: (this: This, ...args: Args) => Return,
|
|
882
|
+
context: ClassMethodDecoratorContext
|
|
883
|
+
) {
|
|
884
|
+
return function (this: This, ...args: Args): Return {
|
|
885
|
+
console.log(`Calling ${String(context.name)} with`, args);
|
|
886
|
+
return target.apply(this, args);
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
class Calculator {
|
|
891
|
+
@logged
|
|
892
|
+
add(a: number, b: number): number {
|
|
893
|
+
return a + b;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// 输出: Calling add with [1, 2]
|
|
898
|
+
new Calculator().add(1, 2);
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
```typescript
|
|
902
|
+
// ⚠️ Stage 3 装饰器与旧版 experimentalDecorators 不同
|
|
903
|
+
// 旧版:tsconfig 中需要 "experimentalDecorators": true
|
|
904
|
+
// 新版(TS 5.0+):默认支持,无需额外配置
|
|
905
|
+
|
|
906
|
+
// ❌ 旧版装饰器签名(仍支持但标记为 legacy)
|
|
907
|
+
function deprecated<T extends { new (...args: any[]): {} }>(constructor: T) {
|
|
908
|
+
return class extends constructor { /* ... */ };
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ✅ 新版装饰器按类型区分 context
|
|
912
|
+
function sealed<T extends { new (...args: any[]): {} }>(
|
|
913
|
+
target: T,
|
|
914
|
+
context: ClassDecoratorContext
|
|
915
|
+
) {
|
|
916
|
+
// context.kind === 'class'
|
|
917
|
+
}
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
### using 声明(显式资源管理,TS 5.2+)
|
|
921
|
+
|
|
922
|
+
```typescript
|
|
923
|
+
// ✅ 使用 Symbol.dispose 实现自动清理
|
|
924
|
+
class TempFile implements Disposable {
|
|
925
|
+
private path: string;
|
|
926
|
+
|
|
927
|
+
constructor() {
|
|
928
|
+
this.path = `/tmp/file-${Date.now()}`;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
write(data: string) { /* ... */ }
|
|
932
|
+
|
|
933
|
+
[Symbol.dispose]() {
|
|
934
|
+
// 自动清理——无论函数如何退出(正常/异常)
|
|
935
|
+
fs.unlinkSync(this.path);
|
|
936
|
+
console.log(`Cleaned up: ${this.path}`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function processFile() {
|
|
941
|
+
using file = new TempFile(); // using 声明
|
|
942
|
+
file.write('data');
|
|
943
|
+
// 作用域结束时自动调用 file[Symbol.dispose]()
|
|
944
|
+
}
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
```typescript
|
|
948
|
+
// ✅ AsyncDisposable 用于异步资源(TS 5.2+)
|
|
949
|
+
class DatabaseConnection implements AsyncDisposable {
|
|
950
|
+
private db: sqlite3.Database;
|
|
951
|
+
|
|
952
|
+
async connect() {
|
|
953
|
+
this.db = new sqlite3.Database(':memory:');
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
async [Symbol.asyncDispose]() {
|
|
957
|
+
await this.db.close();
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
async function query() {
|
|
962
|
+
await using conn = new DatabaseConnection(); // await using
|
|
963
|
+
await conn.connect();
|
|
964
|
+
// 作用域结束时自动 await conn[Symbol.asyncDispose]()
|
|
965
|
+
}
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
### 枚举改进(TS 5.0+)
|
|
969
|
+
|
|
970
|
+
```typescript
|
|
971
|
+
// ✅ 所有枚举现在都是 union 枚举(TS 5.0+)
|
|
972
|
+
enum Color {
|
|
973
|
+
Red = 'RED',
|
|
974
|
+
Green = 'GREEN',
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// 之前:Color 作为类型时行为不一致
|
|
978
|
+
// 现在:Color 完全作为字符串字面量联合类型
|
|
979
|
+
const color: Color = Color.Red; // TypeScript 现在对 Color 类型有更好的推断
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
## Review Checklist
|
|
983
|
+
|
|
984
|
+
### 类型系统
|
|
985
|
+
- [ ] 没有使用 `any`(使用 `unknown` + 类型守卫代替)
|
|
986
|
+
- [ ] 接口和类型定义完整且有意义的命名
|
|
987
|
+
- [ ] 使用泛型提高代码复用性
|
|
988
|
+
- [ ] 联合类型有正确的类型收窄
|
|
989
|
+
- [ ] 善用工具类型(Partial、Pick、Omit 等)
|
|
990
|
+
|
|
991
|
+
### 泛型
|
|
992
|
+
- [ ] 泛型有适当的约束(extends)
|
|
993
|
+
- [ ] 泛型参数有合理的默认值
|
|
994
|
+
- [ ] 避免过度泛型化(KISS 原则)
|
|
995
|
+
|
|
996
|
+
### Strict 模式
|
|
997
|
+
- [ ] tsconfig.json 启用了 strict: true
|
|
998
|
+
- [ ] 启用了 noUncheckedIndexedAccess
|
|
999
|
+
- [ ] 没有使用 @ts-ignore(改用 @ts-expect-error)
|
|
1000
|
+
|
|
1001
|
+
### 异步代码
|
|
1002
|
+
- [ ] async 函数有错误处理
|
|
1003
|
+
- [ ] Promise rejection 被正确处理
|
|
1004
|
+
- [ ] 没有 floating promises(未处理的 Promise)
|
|
1005
|
+
- [ ] 并发请求使用 Promise.all 或 Promise.allSettled
|
|
1006
|
+
- [ ] 竞态条件使用 AbortController 处理
|
|
1007
|
+
|
|
1008
|
+
### 不可变性
|
|
1009
|
+
- [ ] 不直接修改函数参数
|
|
1010
|
+
- [ ] 使用 spread 操作符创建新对象/数组
|
|
1011
|
+
- [ ] 考虑使用 readonly 修饰符
|
|
1012
|
+
|
|
1013
|
+
### ESLint
|
|
1014
|
+
- [ ] 使用 @typescript-eslint/recommended
|
|
1015
|
+
- [ ] 没有 ESLint 警告或错误
|
|
1016
|
+
- [ ] 使用 consistent-type-imports
|