@choblue/claude-code-toolkit 1.1.1 → 1.1.4
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/.claude/.project-map-cache +1 -0
- package/.claude/CLAUDE.md +12 -0
- package/.claude/PROJECT_MAP.md +43 -0
- package/.claude/agents/code-reviewer.md +8 -0
- package/.claude/agents/code-writer-be.md +8 -0
- package/.claude/agents/code-writer-fe.md +8 -0
- package/.claude/agents/explore.md +15 -0
- package/.claude/agents/git-manager.md +8 -0
- package/.claude/agents/test-writer-be.md +8 -0
- package/.claude/agents/test-writer-fe.md +8 -0
- package/.claude/hooks/project-map-detector.sh +71 -0
- package/.claude/hooks/skill-detector.sh +89 -0
- package/.claude/hooks/skill-keywords.conf +19 -0
- package/.claude/scripts/generate-project-map.sh +264 -0
- package/.claude/settings.json +8 -0
- package/.claude/skills/Coding/SKILL.md +5 -0
- package/.claude/skills/NextJS/SKILL.md +5 -0
- package/.claude/skills/React/SKILL.md +5 -0
- package/.claude/skills/ReactHookForm/SKILL.md +5 -0
- package/.claude/skills/TDD/SKILL.md +5 -0
- package/.claude/skills/TailwindCSS/SKILL.md +28 -5
- package/.claude/skills/TanStackQuery/SKILL.md +5 -0
- package/.claude/skills/TypeORM/SKILL.md +16 -303
- package/.claude/skills/TypeORM/references/advanced-queries.md +176 -0
- package/.claude/skills/TypeORM/references/migrations.md +62 -0
- package/.claude/skills/TypeORM/references/transactions.md +76 -0
- package/.claude/skills/TypeScript/SKILL.md +17 -225
- package/.claude/skills/TypeScript/references/advanced-patterns.md +146 -0
- package/.claude/skills/TypeScript/references/generics.md +98 -0
- package/.claude/skills/TypeScript/references/type-guards.md +109 -0
- package/.claude/skills/Zustand/SKILL.md +5 -0
- package/README.md +35 -4
- package/install.sh +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# TypeORM Transactions - 트랜잭션 패턴 및 사용 가이드
|
|
2
|
+
|
|
3
|
+
> 이 문서는 `../SKILL.md`의 참조 문서이다.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. DataSource.transaction() 패턴
|
|
8
|
+
|
|
9
|
+
가장 간결한 트랜잭션 패턴이다. 콜백 내에서 제공되는 `EntityManager`를 사용한다.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
@Injectable()
|
|
13
|
+
export class OrderService {
|
|
14
|
+
constructor(private readonly dataSource: DataSource) {}
|
|
15
|
+
|
|
16
|
+
async createOrder(dto: CreateOrderDto): Promise<Order> {
|
|
17
|
+
return this.dataSource.transaction(async (manager) => {
|
|
18
|
+
const order = manager.create(Order, {
|
|
19
|
+
userId: dto.userId,
|
|
20
|
+
totalAmount: dto.totalAmount,
|
|
21
|
+
});
|
|
22
|
+
await manager.save(order);
|
|
23
|
+
|
|
24
|
+
const orderItems = dto.items.map((item) =>
|
|
25
|
+
manager.create(OrderItem, { ...item, orderId: order.id }),
|
|
26
|
+
);
|
|
27
|
+
await manager.save(orderItems);
|
|
28
|
+
|
|
29
|
+
await manager.decrement(
|
|
30
|
+
Product,
|
|
31
|
+
{ id: In(dto.items.map((i) => i.productId)) },
|
|
32
|
+
'stock',
|
|
33
|
+
1,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return order;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 2. QueryRunner를 사용한 수동 트랜잭션
|
|
45
|
+
|
|
46
|
+
세밀한 제어가 필요한 경우 사용한다.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
async transferPoints(fromId: string, toId: string, amount: number): Promise<void> {
|
|
50
|
+
const queryRunner = this.dataSource.createQueryRunner();
|
|
51
|
+
await queryRunner.connect();
|
|
52
|
+
await queryRunner.startTransaction();
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await queryRunner.manager.decrement(User, { id: fromId }, 'points', amount);
|
|
56
|
+
await queryRunner.manager.increment(User, { id: toId }, 'points', amount);
|
|
57
|
+
|
|
58
|
+
await queryRunner.commitTransaction();
|
|
59
|
+
} catch (error) {
|
|
60
|
+
await queryRunner.rollbackTransaction();
|
|
61
|
+
throw error;
|
|
62
|
+
} finally {
|
|
63
|
+
await queryRunner.release(); // 반드시 release
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 3. 트랜잭션 선택 가이드
|
|
71
|
+
|
|
72
|
+
| 상황 | 권장 패턴 |
|
|
73
|
+
|------|----------|
|
|
74
|
+
| 단순 다중 저장 | `DataSource.transaction()` |
|
|
75
|
+
| 조건부 커밋/롤백 | `QueryRunner` 수동 트랜잭션 |
|
|
76
|
+
| 중간에 외부 API 호출 필요 | `QueryRunner` (커밋 시점 제어) |
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: typescript
|
|
3
|
+
description: TypeScript 고급 패턴 가이드. 타입 추론, 유틸리티 타입, 제네릭, 타입 가드, 고급 타입 패턴 등 TypeScript 코드 작성 시 참조한다.
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# TypeScript Skill - 고급 패턴 규칙
|
|
2
7
|
|
|
3
8
|
FE/BE 공통으로 적용되는 TypeScript 심화 규칙을 정의한다.
|
|
@@ -128,198 +133,7 @@ type UsersResult = Awaited<ReturnType<typeof getUsers>>; // User[]
|
|
|
128
133
|
|
|
129
134
|
---
|
|
130
135
|
|
|
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. 타입 설계 원칙
|
|
136
|
+
## 3. 타입 설계 원칙
|
|
323
137
|
|
|
324
138
|
### 유니언 > enum
|
|
325
139
|
|
|
@@ -391,39 +205,9 @@ type DeepReadonly<T> = {
|
|
|
391
205
|
};
|
|
392
206
|
```
|
|
393
207
|
|
|
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
208
|
---
|
|
425
209
|
|
|
426
|
-
##
|
|
210
|
+
## 4. 비동기 타입
|
|
427
211
|
|
|
428
212
|
### Promise<T>와 async/await 반환 타입
|
|
429
213
|
|
|
@@ -503,7 +287,15 @@ async function fetchDashboard(userId: string): Promise<{
|
|
|
503
287
|
|
|
504
288
|
---
|
|
505
289
|
|
|
506
|
-
##
|
|
290
|
+
## 참조 문서
|
|
291
|
+
|
|
292
|
+
- **[Generics](./references/generics.md)** - 제네릭 함수, 제약 조건, 고급 제네릭 패턴
|
|
293
|
+
- **[Type Guards](./references/type-guards.md)** - 타입 가드, 판별 유니온, 완전성 검사
|
|
294
|
+
- **[Advanced Patterns](./references/advanced-patterns.md)** - 조건부 타입, 매핑 타입, 템플릿 리터럴 타입
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## 5. 금지 사항
|
|
507
299
|
|
|
508
300
|
- `any` 사용 금지 - `unknown`을 사용한 후 타입 가드로 좁힌다
|
|
509
301
|
- `as` 타입 단언 남용 금지 - 타입 가드 또는 올바른 타입 설계로 해결한다 (Branded Type 등 불가피한 경우 제외)
|
|
@@ -525,4 +317,4 @@ const data: unknown = response.body;
|
|
|
525
317
|
if (isUser(data)) {
|
|
526
318
|
const name = data.name; // 타입 가드로 안전하게 접근
|
|
527
319
|
}
|
|
528
|
-
```
|
|
320
|
+
```
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# 고급 타입 패턴 (Advanced Patterns)
|
|
2
|
+
|
|
3
|
+
이 문서는 TypeScript 고급 타입 패턴을 다룬다.
|
|
4
|
+
기본 규칙은 [SKILL.md](../SKILL.md)를 참고한다.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Branded Type (같은 원시 타입이지만 구분해야 할 때)
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
// Bad - UserId와 PostId가 모두 string이라 실수로 혼용 가능
|
|
12
|
+
function getPost(postId: string): Post { /* ... */ }
|
|
13
|
+
getPost(userId); // 컴파일 에러 없음 (런타임 버그)
|
|
14
|
+
|
|
15
|
+
// Good - Branded Type으로 구분
|
|
16
|
+
type Brand<T, B extends string> = T & { readonly __brand: B };
|
|
17
|
+
|
|
18
|
+
type UserId = Brand<string, 'UserId'>;
|
|
19
|
+
type PostId = Brand<string, 'PostId'>;
|
|
20
|
+
|
|
21
|
+
function createUserId(id: string): UserId {
|
|
22
|
+
return id as UserId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createPostId(id: string): PostId {
|
|
26
|
+
return id as PostId;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getPost(postId: PostId): Post { /* ... */ }
|
|
30
|
+
|
|
31
|
+
const userId = createUserId('user-1');
|
|
32
|
+
const postId = createPostId('post-1');
|
|
33
|
+
|
|
34
|
+
getPost(postId); // OK
|
|
35
|
+
// getPost(userId); // 컴파일 에러 - UserId는 PostId에 할당 불가
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 조건부 타입 (Conditional Types)
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// 기본 조건부 타입
|
|
44
|
+
type IsString<T> = T extends string ? true : false;
|
|
45
|
+
|
|
46
|
+
type A = IsString<string>; // true
|
|
47
|
+
type B = IsString<number>; // false
|
|
48
|
+
|
|
49
|
+
// 분배 조건부 타입 (유니언에 자동 분배)
|
|
50
|
+
type NonNullable<T> = T extends null | undefined ? never : T;
|
|
51
|
+
type Result = NonNullable<string | null | undefined>; // string
|
|
52
|
+
|
|
53
|
+
// infer를 이용한 타입 추출
|
|
54
|
+
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
|
|
55
|
+
type Resolved = UnwrapPromise<Promise<string>>; // string
|
|
56
|
+
|
|
57
|
+
// 배열 요소 타입 추출
|
|
58
|
+
type ElementType<T> = T extends (infer E)[] ? E : T;
|
|
59
|
+
type Item = ElementType<string[]>; // string
|
|
60
|
+
|
|
61
|
+
// 함수 반환 타입 추출 (ReturnType 구현)
|
|
62
|
+
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 매핑 타입 (Mapped Types)
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// 기본 매핑 타입
|
|
71
|
+
type Readonly<T> = {
|
|
72
|
+
readonly [P in keyof T]: T[P];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type Partial<T> = {
|
|
76
|
+
[P in keyof T]?: T[P];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// 키 리매핑 (as 절)
|
|
80
|
+
type Getters<T> = {
|
|
81
|
+
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
interface Person {
|
|
85
|
+
name: string;
|
|
86
|
+
age: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type PersonGetters = Getters<Person>;
|
|
90
|
+
// { getName: () => string; getAge: () => number; }
|
|
91
|
+
|
|
92
|
+
// 특정 타입의 키만 필터링
|
|
93
|
+
type StringKeys<T> = {
|
|
94
|
+
[K in keyof T as T[K] extends string ? K : never]: T[K];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
type PersonStringKeys = StringKeys<Person>;
|
|
98
|
+
// { name: string; }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 템플릿 리터럴 타입 (Template Literal Types)
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// 기본 사용
|
|
107
|
+
type EventName = `on${Capitalize<'click' | 'focus' | 'blur'>}`;
|
|
108
|
+
// 'onClick' | 'onFocus' | 'onBlur'
|
|
109
|
+
|
|
110
|
+
// CSS 단위
|
|
111
|
+
type CSSValue = `${number}${'px' | 'rem' | 'em' | '%'}`;
|
|
112
|
+
const width: CSSValue = '100px'; // OK
|
|
113
|
+
// const invalid: CSSValue = '100vw'; // 컴파일 에러
|
|
114
|
+
|
|
115
|
+
// API 경로 타입
|
|
116
|
+
type ApiPath = `/api/${'users' | 'posts' | 'comments'}`;
|
|
117
|
+
type ApiPathWithId = `${ApiPath}/${string}`;
|
|
118
|
+
|
|
119
|
+
// 조합 활용
|
|
120
|
+
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
121
|
+
type Endpoint = `${HTTPMethod} ${ApiPath}`;
|
|
122
|
+
// 'GET /api/users' | 'GET /api/posts' | ... (12개 조합)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 깊은 불변 타입 (Deep Readonly)
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
type DeepReadonly<T> = {
|
|
131
|
+
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
interface NestedConfig {
|
|
135
|
+
database: {
|
|
136
|
+
host: string;
|
|
137
|
+
port: number;
|
|
138
|
+
};
|
|
139
|
+
cache: {
|
|
140
|
+
ttl: number;
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
type FrozenConfig = DeepReadonly<NestedConfig>;
|
|
145
|
+
// database.host, database.port, cache.ttl 모두 readonly
|
|
146
|
+
```
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# 제네릭 (Generics)
|
|
2
|
+
|
|
3
|
+
이 문서는 TypeScript 제네릭의 상세 패턴을 다룬다.
|
|
4
|
+
기본 규칙은 [SKILL.md](../SKILL.md)를 참고한다.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 함수 제네릭
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
// Bad - any 사용
|
|
12
|
+
function first(arr: any[]): any {
|
|
13
|
+
return arr[0];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Good - 제네릭으로 타입 안전성 확보
|
|
17
|
+
function first<T>(arr: T[]): T | undefined {
|
|
18
|
+
return arr[0];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const num = first([1, 2, 3]); // number | undefined
|
|
22
|
+
const str = first(['a', 'b']); // string | undefined
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 제약 조건 (extends)
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// Bad - 모든 타입 허용
|
|
31
|
+
function getProperty<T>(obj: T, key: string): unknown {
|
|
32
|
+
return (obj as Record<string, unknown>)[key];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Good - 제약 조건으로 타입 안전성 확보
|
|
36
|
+
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
|
|
37
|
+
return obj[key];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const user = { name: 'Alice', age: 30 };
|
|
41
|
+
const name = getProperty(user, 'name'); // string
|
|
42
|
+
// getProperty(user, 'invalid'); // 컴파일 에러
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 제네릭 기본값
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
interface PaginatedResponse<T, M = Record<string, never>> {
|
|
51
|
+
data: T[];
|
|
52
|
+
total: number;
|
|
53
|
+
page: number;
|
|
54
|
+
limit: number;
|
|
55
|
+
meta: M;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 메타 정보 없이 사용
|
|
59
|
+
type UserListResponse = PaginatedResponse<User>;
|
|
60
|
+
|
|
61
|
+
// 메타 정보와 함께 사용
|
|
62
|
+
type SearchResponse = PaginatedResponse<User, { query: string; took: number }>;
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 실전 예시 - API 응답 래퍼
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// API 응답 공통 래퍼
|
|
71
|
+
interface ApiResponse<T> {
|
|
72
|
+
success: boolean;
|
|
73
|
+
data: T;
|
|
74
|
+
error: string | null;
|
|
75
|
+
timestamp: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 페이지네이션 래퍼
|
|
79
|
+
interface PaginatedData<T> {
|
|
80
|
+
items: T[];
|
|
81
|
+
total: number;
|
|
82
|
+
page: number;
|
|
83
|
+
pageSize: number;
|
|
84
|
+
hasNext: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 조합하여 사용
|
|
88
|
+
type UserListApiResponse = ApiResponse<PaginatedData<User>>;
|
|
89
|
+
|
|
90
|
+
// 제네릭 API 호출 함수
|
|
91
|
+
async function apiGet<T>(url: string): Promise<ApiResponse<T>> {
|
|
92
|
+
const response = await fetch(url);
|
|
93
|
+
return response.json() as Promise<ApiResponse<T>>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result = await apiGet<User[]>('/api/users');
|
|
97
|
+
// result.data는 User[]로 타입 추론
|
|
98
|
+
```
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# 타입 가드 (Type Guards)
|
|
2
|
+
|
|
3
|
+
이 문서는 TypeScript 타입 가드의 상세 패턴을 다룬다.
|
|
4
|
+
기본 규칙은 [SKILL.md](../SKILL.md)를 참고한다.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## is 키워드 (사용자 정의 타입 가드)
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
interface Admin {
|
|
12
|
+
role: 'admin';
|
|
13
|
+
permissions: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Guest {
|
|
17
|
+
role: 'guest';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type AppUser = Admin | Guest;
|
|
21
|
+
|
|
22
|
+
// Bad - 타입 단언
|
|
23
|
+
function getPermissions(user: AppUser): string[] {
|
|
24
|
+
return (user as Admin).permissions ?? [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Good - 타입 가드
|
|
28
|
+
function isAdmin(user: AppUser): user is Admin {
|
|
29
|
+
return user.role === 'admin';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getPermissions(user: AppUser): string[] {
|
|
33
|
+
if (isAdmin(user)) {
|
|
34
|
+
return user.permissions; // Admin으로 좁혀짐
|
|
35
|
+
}
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## in 연산자
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
interface Dog {
|
|
46
|
+
bark: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface Cat {
|
|
50
|
+
meow: () => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type Pet = Dog | Cat;
|
|
54
|
+
|
|
55
|
+
function makeSound(pet: Pet): void {
|
|
56
|
+
if ('bark' in pet) {
|
|
57
|
+
pet.bark(); // Dog으로 좁혀짐
|
|
58
|
+
} else {
|
|
59
|
+
pet.meow(); // Cat으로 좁혀짐
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Discriminated Union (태그드 유니언)
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// 공통 판별 필드(type)를 가진 유니언
|
|
70
|
+
type Shape =
|
|
71
|
+
| { type: 'circle'; radius: number }
|
|
72
|
+
| { type: 'rectangle'; width: number; height: number }
|
|
73
|
+
| { type: 'triangle'; base: number; height: number };
|
|
74
|
+
|
|
75
|
+
function calculateArea(shape: Shape): number {
|
|
76
|
+
switch (shape.type) {
|
|
77
|
+
case 'circle':
|
|
78
|
+
return Math.PI * shape.radius ** 2;
|
|
79
|
+
case 'rectangle':
|
|
80
|
+
return shape.width * shape.height;
|
|
81
|
+
case 'triangle':
|
|
82
|
+
return (shape.base * shape.height) / 2;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Exhaustive Check (never를 이용한 완전성 검사)
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// 모든 케이스를 처리했는지 컴파일 타임에 검증한다
|
|
93
|
+
function assertNever(value: never): never {
|
|
94
|
+
throw new Error(`Unexpected value: ${value}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getShapeLabel(shape: Shape): string {
|
|
98
|
+
switch (shape.type) {
|
|
99
|
+
case 'circle':
|
|
100
|
+
return '원';
|
|
101
|
+
case 'rectangle':
|
|
102
|
+
return '직사각형';
|
|
103
|
+
case 'triangle':
|
|
104
|
+
return '삼각형';
|
|
105
|
+
default:
|
|
106
|
+
return assertNever(shape); // 새로운 Shape 추가 시 컴파일 에러 발생
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|