@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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/providers/data/.manifest.json +1 -1
  3. package/dist/skills/code-review/LICENSE +21 -0
  4. package/dist/skills/code-review/SKILL.md +233 -0
  5. package/dist/skills/code-review/assets/pr-review-template.md +137 -0
  6. package/dist/skills/code-review/assets/review-checklist.md +123 -0
  7. package/dist/skills/code-review/reference/angular.md +768 -0
  8. package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
  9. package/dist/skills/code-review/reference/c.md +890 -0
  10. package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
  11. package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
  12. package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
  13. package/dist/skills/code-review/reference/cpp.md +893 -0
  14. package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  15. package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
  16. package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
  17. package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
  18. package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
  19. package/dist/skills/code-review/reference/csharp.md +525 -0
  20. package/dist/skills/code-review/reference/css-less-sass.md +661 -0
  21. package/dist/skills/code-review/reference/dart.md +670 -0
  22. package/dist/skills/code-review/reference/django.md +985 -0
  23. package/dist/skills/code-review/reference/fastapi.md +580 -0
  24. package/dist/skills/code-review/reference/go.md +993 -0
  25. package/dist/skills/code-review/reference/java.md +409 -0
  26. package/dist/skills/code-review/reference/java8.md +586 -0
  27. package/dist/skills/code-review/reference/kotlin.md +1018 -0
  28. package/dist/skills/code-review/reference/nestjs.md +593 -0
  29. package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
  30. package/dist/skills/code-review/reference/php.md +684 -0
  31. package/dist/skills/code-review/reference/python.md +1073 -0
  32. package/dist/skills/code-review/reference/qt.md +757 -0
  33. package/dist/skills/code-review/reference/react.md +871 -0
  34. package/dist/skills/code-review/reference/ruby.md +964 -0
  35. package/dist/skills/code-review/reference/rust.md +846 -0
  36. package/dist/skills/code-review/reference/security-review-guide.md +494 -0
  37. package/dist/skills/code-review/reference/svelte.md +1064 -0
  38. package/dist/skills/code-review/reference/swift.md +936 -0
  39. package/dist/skills/code-review/reference/typescript.md +1016 -0
  40. package/dist/skills/code-review/reference/vue.md +924 -0
  41. package/dist/skills/code-review/reference/zig.md +440 -0
  42. package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
  43. package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
  44. package/dist/yorn.cjs +628 -628
  45. 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