@choblue/claude-code-toolkit 1.0.0

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.
@@ -0,0 +1,528 @@
1
+ # TypeScript Skill - 고급 패턴 규칙
2
+
3
+ FE/BE 공통으로 적용되는 TypeScript 심화 규칙을 정의한다.
4
+ 공통 코딩 원칙은 `../Coding/SKILL.md`를 함께 참고한다.
5
+
6
+ ---
7
+
8
+ ## 1. 타입 추론 활용
9
+
10
+ ### 변수는 추론에 맡긴다
11
+
12
+ ```typescript
13
+ // Bad - 불필요한 타입 명시
14
+ const name: string = 'Alice';
15
+ const count: number = 0;
16
+ const isActive: boolean = true;
17
+ const users: User[] = [user1, user2];
18
+
19
+ // Good - 타입 추론에 맡김
20
+ const name = 'Alice';
21
+ const count = 0;
22
+ const isActive = true;
23
+ const users = [user1, user2];
24
+ ```
25
+
26
+ ### 함수 반환 타입과 파라미터는 명시한다
27
+
28
+ ```typescript
29
+ // Bad - 반환 타입 누락 (호출자가 추론에 의존해야 함)
30
+ function findUser(id: string) {
31
+ return users.find((user) => user.id === id);
32
+ }
33
+
34
+ // Good - 반환 타입 명시
35
+ function findUser(id: string): User | undefined {
36
+ return users.find((user) => user.id === id);
37
+ }
38
+
39
+ // Good - 파라미터 타입 명시
40
+ function calculateTotal(items: CartItem[]): number {
41
+ return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
42
+ }
43
+ ```
44
+
45
+ ### as const를 활용한다
46
+
47
+ ```typescript
48
+ // Bad - 타입이 string[]으로 추론됨
49
+ const ROLES = ['admin', 'editor', 'viewer'];
50
+
51
+ // Good - readonly ['admin', 'editor', 'viewer']로 추론됨
52
+ const ROLES = ['admin', 'editor', 'viewer'] as const;
53
+ type Role = (typeof ROLES)[number]; // 'admin' | 'editor' | 'viewer'
54
+
55
+ // Good - 객체에도 활용
56
+ const HTTP_STATUS = {
57
+ OK: 200,
58
+ NOT_FOUND: 404,
59
+ INTERNAL_ERROR: 500,
60
+ } as const;
61
+ type HttpStatus = (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS]; // 200 | 404 | 500
62
+ ```
63
+
64
+ ### satisfies를 활용한다
65
+
66
+ ```typescript
67
+ // Bad - 타입 명시로 인해 추론이 사라짐
68
+ const config: Record<string, string> = {
69
+ apiUrl: 'https://api.example.com',
70
+ timeout: '3000', // 실수로 string을 넣어도 감지 못함
71
+ };
72
+
73
+ // Good - satisfies로 타입 검증 + 추론 유지
74
+ const config = {
75
+ apiUrl: 'https://api.example.com',
76
+ timeout: 3000,
77
+ } satisfies Record<string, string | number>;
78
+ // config.apiUrl의 타입이 string으로 추론됨 (Record<string, string | number>가 아님)
79
+ ```
80
+
81
+ ---
82
+
83
+ ## 2. 유틸리티 타입
84
+
85
+ ### 자주 사용하는 유틸리티 타입
86
+
87
+ | 유틸리티 타입 | 설명 | 용례 |
88
+ |---------------|------|------|
89
+ | `Pick<T, K>` | 특정 프로퍼티만 선택 | API 응답에서 필요한 필드만 추출 |
90
+ | `Omit<T, K>` | 특정 프로퍼티를 제외 | 생성 DTO에서 id 제외 |
91
+ | `Partial<T>` | 모든 프로퍼티를 선택적으로 | 업데이트 DTO |
92
+ | `Required<T>` | 모든 프로퍼티를 필수로 | 기본값 적용 후 타입 |
93
+ | `Record<K, V>` | 키-값 매핑 | 딕셔너리, 룩업 테이블 |
94
+ | `Exclude<U, E>` | 유니언에서 특정 타입 제외 | 특정 상태 제외 |
95
+ | `Extract<U, E>` | 유니언에서 특정 타입 추출 | 특정 상태만 추출 |
96
+ | `ReturnType<F>` | 함수의 반환 타입 추출 | 함수 결과 타입 재사용 |
97
+ | `Parameters<F>` | 함수의 매개변수 타입 추출 | 래퍼 함수 작성 시 |
98
+ | `Awaited<T>` | Promise를 풀어낸 타입 | async 함수 결과 타입 |
99
+ | `NonNullable<T>` | null/undefined 제거 | 필터링 후 타입 |
100
+
101
+ ### 실전 예시
102
+
103
+ ```typescript
104
+ interface User {
105
+ id: string;
106
+ name: string;
107
+ email: string;
108
+ password: string;
109
+ createdAt: Date;
110
+ updatedAt: Date;
111
+ }
112
+
113
+ // 생성 시 id, 날짜는 서버에서 생성
114
+ type CreateUserInput = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
115
+
116
+ // 수정 시 모든 필드 선택적
117
+ type UpdateUserInput = Partial<Pick<User, 'name' | 'email'>>;
118
+
119
+ // 공개 프로필 (비밀번호 제외)
120
+ type UserProfile = Omit<User, 'password'>;
121
+
122
+ // 함수 반환 타입 재사용
123
+ function getUsers() {
124
+ return fetch('/api/users').then((res) => res.json() as Promise<User[]>);
125
+ }
126
+ type UsersResult = Awaited<ReturnType<typeof getUsers>>; // User[]
127
+ ```
128
+
129
+ ---
130
+
131
+ ## 3. 제네릭
132
+
133
+ ### 함수 제네릭
134
+
135
+ ```typescript
136
+ // Bad - any 사용
137
+ function first(arr: any[]): any {
138
+ return arr[0];
139
+ }
140
+
141
+ // Good - 제네릭으로 타입 안전성 확보
142
+ function first<T>(arr: T[]): T | undefined {
143
+ return arr[0];
144
+ }
145
+
146
+ const num = first([1, 2, 3]); // number | undefined
147
+ const str = first(['a', 'b']); // string | undefined
148
+ ```
149
+
150
+ ### 제약 조건 (extends)
151
+
152
+ ```typescript
153
+ // Bad - 모든 타입 허용
154
+ function getProperty<T>(obj: T, key: string): unknown {
155
+ return (obj as Record<string, unknown>)[key];
156
+ }
157
+
158
+ // Good - 제약 조건으로 타입 안전성 확보
159
+ function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
160
+ return obj[key];
161
+ }
162
+
163
+ const user = { name: 'Alice', age: 30 };
164
+ const name = getProperty(user, 'name'); // string
165
+ // getProperty(user, 'invalid'); // 컴파일 에러
166
+ ```
167
+
168
+ ### 제네릭 기본값
169
+
170
+ ```typescript
171
+ interface PaginatedResponse<T, M = Record<string, never>> {
172
+ data: T[];
173
+ total: number;
174
+ page: number;
175
+ limit: number;
176
+ meta: M;
177
+ }
178
+
179
+ // 메타 정보 없이 사용
180
+ type UserListResponse = PaginatedResponse<User>;
181
+
182
+ // 메타 정보와 함께 사용
183
+ type SearchResponse = PaginatedResponse<User, { query: string; took: number }>;
184
+ ```
185
+
186
+ ### 실전 예시 - API 응답 래퍼
187
+
188
+ ```typescript
189
+ // API 응답 공통 래퍼
190
+ interface ApiResponse<T> {
191
+ success: boolean;
192
+ data: T;
193
+ error: string | null;
194
+ timestamp: number;
195
+ }
196
+
197
+ // 페이지네이션 래퍼
198
+ interface PaginatedData<T> {
199
+ items: T[];
200
+ total: number;
201
+ page: number;
202
+ pageSize: number;
203
+ hasNext: boolean;
204
+ }
205
+
206
+ // 조합하여 사용
207
+ type UserListApiResponse = ApiResponse<PaginatedData<User>>;
208
+
209
+ // 제네릭 API 호출 함수
210
+ async function apiGet<T>(url: string): Promise<ApiResponse<T>> {
211
+ const response = await fetch(url);
212
+ return response.json() as Promise<ApiResponse<T>>;
213
+ }
214
+
215
+ const result = await apiGet<User[]>('/api/users');
216
+ // result.data는 User[]로 타입 추론
217
+ ```
218
+
219
+ ---
220
+
221
+ ## 4. 타입 가드
222
+
223
+ ### is 키워드 (사용자 정의 타입 가드)
224
+
225
+ ```typescript
226
+ interface Admin {
227
+ role: 'admin';
228
+ permissions: string[];
229
+ }
230
+
231
+ interface Guest {
232
+ role: 'guest';
233
+ }
234
+
235
+ type AppUser = Admin | Guest;
236
+
237
+ // Bad - 타입 단언
238
+ function getPermissions(user: AppUser): string[] {
239
+ return (user as Admin).permissions ?? [];
240
+ }
241
+
242
+ // Good - 타입 가드
243
+ function isAdmin(user: AppUser): user is Admin {
244
+ return user.role === 'admin';
245
+ }
246
+
247
+ function getPermissions(user: AppUser): string[] {
248
+ if (isAdmin(user)) {
249
+ return user.permissions; // Admin으로 좁혀짐
250
+ }
251
+ return [];
252
+ }
253
+ ```
254
+
255
+ ### in 연산자
256
+
257
+ ```typescript
258
+ interface Dog {
259
+ bark: () => void;
260
+ }
261
+
262
+ interface Cat {
263
+ meow: () => void;
264
+ }
265
+
266
+ type Pet = Dog | Cat;
267
+
268
+ function makeSound(pet: Pet): void {
269
+ if ('bark' in pet) {
270
+ pet.bark(); // Dog으로 좁혀짐
271
+ } else {
272
+ pet.meow(); // Cat으로 좁혀짐
273
+ }
274
+ }
275
+ ```
276
+
277
+ ### Discriminated Union (태그드 유니언)
278
+
279
+ ```typescript
280
+ // 공통 판별 필드(type)를 가진 유니언
281
+ type Shape =
282
+ | { type: 'circle'; radius: number }
283
+ | { type: 'rectangle'; width: number; height: number }
284
+ | { type: 'triangle'; base: number; height: number };
285
+
286
+ function calculateArea(shape: Shape): number {
287
+ switch (shape.type) {
288
+ case 'circle':
289
+ return Math.PI * shape.radius ** 2;
290
+ case 'rectangle':
291
+ return shape.width * shape.height;
292
+ case 'triangle':
293
+ return (shape.base * shape.height) / 2;
294
+ }
295
+ }
296
+ ```
297
+
298
+ ### Exhaustive Check (never를 이용한 완전성 검사)
299
+
300
+ ```typescript
301
+ // 모든 케이스를 처리했는지 컴파일 타임에 검증한다
302
+ function assertNever(value: never): never {
303
+ throw new Error(`Unexpected value: ${value}`);
304
+ }
305
+
306
+ function getShapeLabel(shape: Shape): string {
307
+ switch (shape.type) {
308
+ case 'circle':
309
+ return '원';
310
+ case 'rectangle':
311
+ return '직사각형';
312
+ case 'triangle':
313
+ return '삼각형';
314
+ default:
315
+ return assertNever(shape); // 새로운 Shape 추가 시 컴파일 에러 발생
316
+ }
317
+ }
318
+ ```
319
+
320
+ ---
321
+
322
+ ## 5. 타입 설계 원칙
323
+
324
+ ### 유니언 > enum
325
+
326
+ enum 대신 const object 또는 union literal을 사용한다.
327
+
328
+ ```typescript
329
+ // Bad - enum (트리셰이킹 불가, 런타임 코드 생성)
330
+ enum Status {
331
+ Active = 'ACTIVE',
332
+ Inactive = 'INACTIVE',
333
+ Pending = 'PENDING',
334
+ }
335
+
336
+ // Good - const object (런타임 값이 필요할 때)
337
+ const STATUS = {
338
+ Active: 'ACTIVE',
339
+ Inactive: 'INACTIVE',
340
+ Pending: 'PENDING',
341
+ } as const;
342
+ type Status = (typeof STATUS)[keyof typeof STATUS]; // 'ACTIVE' | 'INACTIVE' | 'PENDING'
343
+
344
+ // Good - union literal (런타임 값이 불필요할 때)
345
+ type Status = 'ACTIVE' | 'INACTIVE' | 'PENDING';
346
+ ```
347
+
348
+ ### interface vs type
349
+
350
+ | 용도 | 선택 | 이유 |
351
+ |------|------|------|
352
+ | 객체 형태 정의 | `interface` | 확장 가능, 선언 병합 지원 |
353
+ | 유니언/교차 타입 | `type` | interface로 표현 불가 |
354
+ | 함수 시그니처 | `type` | 간결한 표현 |
355
+ | 튜플 | `type` | interface로 표현 불가 |
356
+ | 기본형 별칭 | `type` | interface로 표현 불가 |
357
+ | Props/DTO 등 객체 | `interface` | 일관성 |
358
+
359
+ ```typescript
360
+ // Good - 객체는 interface
361
+ interface UserProfile {
362
+ name: string;
363
+ email: string;
364
+ }
365
+
366
+ // Good - 유니언/조합은 type
367
+ type Result<T> = { success: true; data: T } | { success: false; error: string };
368
+ type EventHandler = (event: Event) => void;
369
+ type Coordinate = [number, number];
370
+ ```
371
+
372
+ ### readonly 활용 (불변 데이터)
373
+
374
+ ```typescript
375
+ // 객체 불변
376
+ interface Config {
377
+ readonly apiUrl: string;
378
+ readonly timeout: number;
379
+ }
380
+
381
+ // 배열 불변
382
+ function processItems(items: readonly string[]): void {
383
+ // items.push('new'); // 컴파일 에러
384
+ // items[0] = 'modified'; // 컴파일 에러
385
+ const filtered = items.filter((item) => item.length > 0); // OK (새 배열 반환)
386
+ }
387
+
388
+ // 깊은 불변 (Readonly 재귀)
389
+ type DeepReadonly<T> = {
390
+ readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
391
+ };
392
+ ```
393
+
394
+ ### Branded Type (같은 원시 타입이지만 구분해야 할 때)
395
+
396
+ ```typescript
397
+ // Bad - UserId와 PostId가 모두 string이라 실수로 혼용 가능
398
+ function getPost(postId: string): Post { /* ... */ }
399
+ getPost(userId); // 컴파일 에러 없음 (런타임 버그)
400
+
401
+ // Good - Branded Type으로 구분
402
+ type Brand<T, B extends string> = T & { readonly __brand: B };
403
+
404
+ type UserId = Brand<string, 'UserId'>;
405
+ type PostId = Brand<string, 'PostId'>;
406
+
407
+ function createUserId(id: string): UserId {
408
+ return id as UserId;
409
+ }
410
+
411
+ function createPostId(id: string): PostId {
412
+ return id as PostId;
413
+ }
414
+
415
+ function getPost(postId: PostId): Post { /* ... */ }
416
+
417
+ const userId = createUserId('user-1');
418
+ const postId = createPostId('post-1');
419
+
420
+ getPost(postId); // OK
421
+ // getPost(userId); // 컴파일 에러 - UserId는 PostId에 할당 불가
422
+ ```
423
+
424
+ ---
425
+
426
+ ## 6. 비동기 타입
427
+
428
+ ### Promise<T>와 async/await 반환 타입
429
+
430
+ ```typescript
431
+ // Good - async 함수 반환 타입 명시
432
+ async function fetchUser(id: string): Promise<User> {
433
+ const response = await fetch(`/api/users/${id}`);
434
+ return response.json() as Promise<User>;
435
+ }
436
+
437
+ // Good - 에러 가능성이 있는 비동기 함수
438
+ type AsyncResult<T> = Promise<
439
+ { success: true; data: T } | { success: false; error: string }
440
+ >;
441
+
442
+ async function safeFetchUser(id: string): AsyncResult<User> {
443
+ try {
444
+ const user = await fetchUser(id);
445
+ return { success: true, data: user };
446
+ } catch (e) {
447
+ return { success: false, error: e instanceof Error ? e.message : '알 수 없는 오류' };
448
+ }
449
+ }
450
+ ```
451
+
452
+ ### 에러 타입 처리
453
+
454
+ ```typescript
455
+ // Bad - catch에서 any 사용
456
+ try {
457
+ await fetchData();
458
+ } catch (e: any) {
459
+ console.error(e.message);
460
+ }
461
+
462
+ // Good - unknown + 타입 가드
463
+ try {
464
+ await fetchData();
465
+ } catch (e: unknown) {
466
+ if (e instanceof Error) {
467
+ console.error(e.message);
468
+ } else {
469
+ console.error('알 수 없는 오류', e);
470
+ }
471
+ }
472
+
473
+ // Good - 에러 타입 가드 유틸 함수
474
+ function isError(value: unknown): value is Error {
475
+ return value instanceof Error;
476
+ }
477
+
478
+ function getErrorMessage(error: unknown): string {
479
+ if (isError(error)) return error.message;
480
+ if (typeof error === 'string') return error;
481
+ return '알 수 없는 오류가 발생했습니다';
482
+ }
483
+ ```
484
+
485
+ ### 병렬 비동기 처리 타입
486
+
487
+ ```typescript
488
+ // 여러 비동기 작업의 결과 타입
489
+ async function fetchDashboard(userId: string): Promise<{
490
+ user: User;
491
+ posts: Post[];
492
+ notifications: Notification[];
493
+ }> {
494
+ const [user, posts, notifications] = await Promise.all([
495
+ fetchUser(userId),
496
+ fetchPosts(userId),
497
+ fetchNotifications(userId),
498
+ ]);
499
+
500
+ return { user, posts, notifications };
501
+ }
502
+ ```
503
+
504
+ ---
505
+
506
+ ## 7. 금지 사항
507
+
508
+ - `any` 사용 금지 - `unknown`을 사용한 후 타입 가드로 좁힌다
509
+ - `as` 타입 단언 남용 금지 - 타입 가드 또는 올바른 타입 설계로 해결한다 (Branded Type 등 불가피한 경우 제외)
510
+ - `@ts-ignore` 사용 금지 - 타입 오류를 무시하지 않고 근본 원인을 해결한다
511
+ - `@ts-expect-error` 남용 금지 - 테스트 코드에서 의도적 에러 검증 시에만 허용한다
512
+ - non-null assertion (`!`) 남용 금지 - 옵셔널 체이닝(`?.`) 또는 타입 가드를 사용한다
513
+ - `enum` 사용 금지 - const object 또는 union literal을 사용한다
514
+ - 빈 인터페이스 `{}` 사용 금지 - `Record<string, never>` 또는 `unknown`을 사용한다
515
+
516
+ ```typescript
517
+ // Bad
518
+ const data: any = response.body;
519
+ const user = data as User;
520
+ // @ts-ignore
521
+ const name = user!.name;
522
+
523
+ // Good
524
+ const data: unknown = response.body;
525
+ if (isUser(data)) {
526
+ const name = data.name; // 타입 가드로 안전하게 접근
527
+ }
528
+ ```