codecruise 0.1.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.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/codecruise.js +68 -0
- package/config/CLAUDE.md +107 -0
- package/config/agents/analyst.md +48 -0
- package/config/agents/architect-reviewer.md +161 -0
- package/config/agents/architect.md +119 -0
- package/config/agents/critic.md +63 -0
- package/config/agents/developer.md +96 -0
- package/config/agents/devops.md +81 -0
- package/config/agents/orchestrator.md +91 -0
- package/config/agents/planner.md +139 -0
- package/config/agents/retro.md +52 -0
- package/config/agents/reviewer.md +101 -0
- package/config/agents/security-reviewer.md +57 -0
- package/config/agents/stack/expo/AGENT.md +473 -0
- package/config/agents/stack/expo/rules/critical.md +427 -0
- package/config/agents/stack/expo/rules/native.md +455 -0
- package/config/agents/stack/expo/rules/navigation.md +445 -0
- package/config/agents/stack/expo/rules/performance.md +415 -0
- package/config/agents/stack/fastify/AGENT.md +397 -0
- package/config/agents/stack/fastify/rules/api-design.md +283 -0
- package/config/agents/stack/fastify/rules/critical.md +232 -0
- package/config/agents/stack/fastify/rules/queues.md +303 -0
- package/config/agents/stack/fastify/rules/security.md +384 -0
- package/config/agents/stack/index.yaml +48 -0
- package/config/agents/stack/nextjs/AGENT.md +421 -0
- package/config/agents/stack/nextjs/rules/components.md +413 -0
- package/config/agents/stack/nextjs/rules/critical.md +391 -0
- package/config/agents/stack/nextjs/rules/performance.md +403 -0
- package/config/agents/stack/nextjs/rules/styling.md +334 -0
- package/config/agents/stack/shared-ts/AGENT.md +384 -0
- package/config/agents/stack/shared-ts/rules/critical.md +315 -0
- package/config/agents/stack/shared-ts/rules/patterns.md +384 -0
- package/config/agents/stack/shared-ts/rules/zod.md +427 -0
- package/config/agents/tester.md +79 -0
- package/config/commands/architect-discuss.md +366 -0
- package/config/commands/architect-list.md +160 -0
- package/config/commands/architect-review.md +111 -0
- package/config/commands/architect.md +118 -0
- package/config/commands/compact.md +118 -0
- package/config/commands/companion.md +279 -0
- package/config/commands/dashboard.md +152 -0
- package/config/commands/doctor.md +227 -0
- package/config/commands/dogfood-report.md +101 -0
- package/config/commands/flags/run-autonomous.md +110 -0
- package/config/commands/flags/run-pause.md +80 -0
- package/config/commands/ingest.md +173 -0
- package/config/commands/init.md +128 -0
- package/config/commands/metrics.md +87 -0
- package/config/commands/parallel.md +320 -0
- package/config/commands/pause.md +55 -0
- package/config/commands/plan-review.md +130 -0
- package/config/commands/plan.md +216 -0
- package/config/commands/production-check.md +308 -0
- package/config/commands/refine.md +323 -0
- package/config/commands/resume.md +72 -0
- package/config/commands/retro.md +121 -0
- package/config/commands/retry.md +75 -0
- package/config/commands/role.md +310 -0
- package/config/commands/run.md +417 -0
- package/config/commands/scope.md +85 -0
- package/config/commands/setup-permissions.md +104 -0
- package/config/commands/skip.md +75 -0
- package/config/commands/spec-forge.md +213 -0
- package/config/commands/spec-help.md +194 -0
- package/config/commands/spec-patch.md +342 -0
- package/config/commands/spec-resolve.md +110 -0
- package/config/commands/spec-review.md +153 -0
- package/config/commands/status.md +114 -0
- package/config/commands/sync.md +131 -0
- package/config/commands/task.md +138 -0
- package/config/commands/verify.md +124 -0
- package/config/hooks/README.md +632 -0
- package/config/hooks/activity-log.sh +187 -0
- package/config/hooks/anti-rationalize.sh +52 -0
- package/config/hooks/capture-verification.sh +112 -0
- package/config/hooks/collect-metrics.sh +135 -0
- package/config/hooks/enforce-file-scope.sh +75 -0
- package/config/hooks/enforce-state-machine.sh +161 -0
- package/config/hooks/enforce-tdd.sh +180 -0
- package/config/hooks/format.sh +40 -0
- package/config/hooks/lib/activity-helpers.sh +162 -0
- package/config/hooks/lib/read-settings.sh +71 -0
- package/config/hooks/load-context-skills.sh +95 -0
- package/config/hooks/notify.sh +81 -0
- package/config/hooks/pre-commit.sample +35 -0
- package/config/hooks/protect-files.sh +63 -0
- package/config/hooks/track-agents.sh +41 -0
- package/config/hooks/track-commands.sh +37 -0
- package/config/hooks/track-enforcement.sh +44 -0
- package/config/hooks/track-ooda.sh +77 -0
- package/config/hooks/validate-commit-msg.sh +35 -0
- package/config/hooks/validate-plan.sh +213 -0
- package/config/hooks/verify-criteria.sh +46 -0
- package/config/hooks/verify-todo-completion.sh +140 -0
- package/config/rules/comments.md +25 -0
- package/config/rules/decision-rules.md +308 -0
- package/config/rules/hygiene.md +247 -0
- package/config/rules/pattern-detection.md +372 -0
- package/config/rules/profiles.md +193 -0
- package/config/rules/recovery.md +83 -0
- package/config/rules/scope-detection.md +213 -0
- package/config/rules/standards.md +127 -0
- package/config/rules/workflow.md +121 -0
- package/config/schemas.md +767 -0
- package/config/settings.json +195 -0
- package/config/skills/backend/SKILL.md +734 -0
- package/config/skills/database/SKILL.md +426 -0
- package/config/skills/frontend/SKILL.md +434 -0
- package/config/skills/git/SKILL.md +396 -0
- package/config/skills/index.yaml +36 -0
- package/config/skills/observability/SKILL.md +430 -0
- package/config/skills/package-dev/SKILL.md +498 -0
- package/config/skills/performance/SKILL.md +378 -0
- package/config/skills/resilience/SKILL.md +573 -0
- package/config/skills/testing/SKILL.md +398 -0
- package/config/skills/testing-patterns/SKILL.md +276 -0
- package/config/skills/typescript/SKILL.md +152 -0
- package/config/templates/CLAUDE.md +70 -0
- package/config/templates/README.md +117 -0
- package/config/templates/steering/adr-template.md +102 -0
- package/config/templates/steering/product.md +60 -0
- package/config/templates/steering/rfc-template.md +159 -0
- package/config/templates/steering/structure.md +146 -0
- package/config/templates/steering/tech.md +85 -0
- package/package.json +40 -0
- package/src/install.js +163 -0
- package/src/report.js +310 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# Critical Rules - TypeScript
|
|
2
|
+
|
|
3
|
+
Must-follow rules for type safety. Violations block PR merge.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Strict Mode
|
|
8
|
+
|
|
9
|
+
### TS-001: Enable all strict flags
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
// tsconfig.json - REQUIRED settings
|
|
13
|
+
{
|
|
14
|
+
"compilerOptions": {
|
|
15
|
+
"strict": true,
|
|
16
|
+
"noUncheckedIndexedAccess": true,
|
|
17
|
+
"noImplicitOverride": true,
|
|
18
|
+
"forceConsistentCasingInFileNames": true,
|
|
19
|
+
"exactOptionalPropertyTypes": true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### TS-002: Never use `any`
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// BAD
|
|
28
|
+
function process(data: any) { ... }
|
|
29
|
+
const result = response as any;
|
|
30
|
+
|
|
31
|
+
// GOOD
|
|
32
|
+
function process(data: unknown) {
|
|
33
|
+
if (isValidData(data)) { ... }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// If truly unknown, use unknown and narrow
|
|
37
|
+
function parseJson(text: string): unknown {
|
|
38
|
+
return JSON.parse(text);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### TS-003: Never use non-null assertion carelessly
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// BAD - crashes if undefined
|
|
46
|
+
const user = users.find(u => u.id === id)!;
|
|
47
|
+
const name = user.name!;
|
|
48
|
+
|
|
49
|
+
// GOOD - handle undefined
|
|
50
|
+
const user = users.find(u => u.id === id);
|
|
51
|
+
if (!user) throw new NotFoundError('User');
|
|
52
|
+
|
|
53
|
+
// Or with nullish coalescing
|
|
54
|
+
const name = user.name ?? 'Unknown';
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Type Safety
|
|
60
|
+
|
|
61
|
+
### TS-004: Prefer explicit return types for public APIs
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// BAD - inferred types can change unexpectedly
|
|
65
|
+
export function getUser(id: string) {
|
|
66
|
+
return db.user.findUnique({ where: { id } });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// GOOD - explicit contract
|
|
70
|
+
export function getUser(id: string): Promise<User | null> {
|
|
71
|
+
return db.user.findUnique({ where: { id } });
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### TS-005: Use const assertions for literals
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// BAD - type is string[]
|
|
79
|
+
const statuses = ['pending', 'active', 'done'];
|
|
80
|
+
|
|
81
|
+
// GOOD - type is readonly ['pending', 'active', 'done']
|
|
82
|
+
const statuses = ['pending', 'active', 'done'] as const;
|
|
83
|
+
|
|
84
|
+
// Useful for type derivation
|
|
85
|
+
type Status = typeof statuses[number]; // 'pending' | 'active' | 'done'
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### TS-006: Avoid type assertions except for narrowing
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// BAD - bypasses type checking
|
|
92
|
+
const user = response as User;
|
|
93
|
+
|
|
94
|
+
// GOOD - validate first
|
|
95
|
+
const user = userSchema.parse(response);
|
|
96
|
+
|
|
97
|
+
// OK - narrowing after check
|
|
98
|
+
if (isUser(response)) {
|
|
99
|
+
const user = response; // Type is narrowed
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// OK - when TypeScript can't infer
|
|
103
|
+
const element = document.getElementById('root') as HTMLDivElement | null;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Null Safety
|
|
109
|
+
|
|
110
|
+
### TS-007: Handle all nullable values
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// BAD - might be undefined
|
|
114
|
+
const items = data.items.map(i => i.name);
|
|
115
|
+
|
|
116
|
+
// GOOD - check first
|
|
117
|
+
const items = data.items?.map(i => i.name) ?? [];
|
|
118
|
+
|
|
119
|
+
// Or throw if required
|
|
120
|
+
if (!data.items) throw new Error('Items required');
|
|
121
|
+
const items = data.items.map(i => i.name);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### TS-008: Use noUncheckedIndexedAccess
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// With noUncheckedIndexedAccess: true
|
|
128
|
+
|
|
129
|
+
const arr = [1, 2, 3];
|
|
130
|
+
const first = arr[0]; // Type is number | undefined
|
|
131
|
+
|
|
132
|
+
// Must handle
|
|
133
|
+
if (first !== undefined) {
|
|
134
|
+
console.log(first + 1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Or use at() with check
|
|
138
|
+
const last = arr.at(-1);
|
|
139
|
+
if (last !== undefined) { ... }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### TS-009: Distinguish null vs undefined
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// null = intentionally empty
|
|
146
|
+
// undefined = not set
|
|
147
|
+
|
|
148
|
+
interface User {
|
|
149
|
+
name: string;
|
|
150
|
+
nickname: string | null; // User chose no nickname
|
|
151
|
+
deletedAt?: Date; // Not deleted = undefined
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check appropriately
|
|
155
|
+
if (user.nickname === null) {
|
|
156
|
+
// User explicitly has no nickname
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (user.deletedAt === undefined) {
|
|
160
|
+
// User is not deleted
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Imports & Exports
|
|
167
|
+
|
|
168
|
+
### TS-010: Use type imports for types
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// BAD - imports at runtime
|
|
172
|
+
import { User, UserService } from './user';
|
|
173
|
+
|
|
174
|
+
// GOOD - type-only imports
|
|
175
|
+
import type { User } from './user';
|
|
176
|
+
import { UserService } from './user';
|
|
177
|
+
|
|
178
|
+
// Or combined
|
|
179
|
+
import { UserService, type User } from './user';
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### TS-011: Export types alongside values
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// BAD - types not exported
|
|
186
|
+
const userSchema = z.object({ ... });
|
|
187
|
+
// User type only available internally
|
|
188
|
+
|
|
189
|
+
// GOOD - export both
|
|
190
|
+
export const userSchema = z.object({ ... });
|
|
191
|
+
export type User = z.infer<typeof userSchema>;
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### TS-012: Use barrel exports carefully
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// index.ts
|
|
198
|
+
export { userSchema, type User } from './user';
|
|
199
|
+
export { orderSchema, type Order } from './order';
|
|
200
|
+
|
|
201
|
+
// Avoid re-exporting everything
|
|
202
|
+
// BAD - pulls in everything
|
|
203
|
+
export * from './user';
|
|
204
|
+
export * from './order';
|
|
205
|
+
|
|
206
|
+
// GOOD - explicit exports
|
|
207
|
+
export { User, userSchema, createUser } from './user';
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Async Safety
|
|
213
|
+
|
|
214
|
+
### TS-013: Always handle Promise rejections
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// BAD - unhandled rejection
|
|
218
|
+
someAsyncFunction();
|
|
219
|
+
|
|
220
|
+
// GOOD - handle or await
|
|
221
|
+
await someAsyncFunction();
|
|
222
|
+
|
|
223
|
+
// Or with catch
|
|
224
|
+
someAsyncFunction().catch(handleError);
|
|
225
|
+
|
|
226
|
+
// In event handlers
|
|
227
|
+
button.addEventListener('click', async () => {
|
|
228
|
+
try {
|
|
229
|
+
await handleClick();
|
|
230
|
+
} catch (error) {
|
|
231
|
+
showError(error);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### TS-014: Type async function returns
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// BAD
|
|
240
|
+
async function fetchUser(id: string) {
|
|
241
|
+
const response = await fetch(`/users/${id}`);
|
|
242
|
+
return response.json();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// GOOD
|
|
246
|
+
async function fetchUser(id: string): Promise<User> {
|
|
247
|
+
const response = await fetch(`/users/${id}`);
|
|
248
|
+
const data = await response.json();
|
|
249
|
+
return userSchema.parse(data);
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Error Handling
|
|
256
|
+
|
|
257
|
+
### TS-015: Use typed errors
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// Define error types
|
|
261
|
+
class AppError extends Error {
|
|
262
|
+
constructor(
|
|
263
|
+
message: string,
|
|
264
|
+
public code: string,
|
|
265
|
+
public statusCode: number = 500
|
|
266
|
+
) {
|
|
267
|
+
super(message);
|
|
268
|
+
this.name = 'AppError';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
class NotFoundError extends AppError {
|
|
273
|
+
constructor(resource: string, id?: string) {
|
|
274
|
+
super(
|
|
275
|
+
id ? `${resource} ${id} not found` : `${resource} not found`,
|
|
276
|
+
'NOT_FOUND',
|
|
277
|
+
404
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
class ValidationError extends AppError {
|
|
283
|
+
constructor(
|
|
284
|
+
message: string,
|
|
285
|
+
public details: Array<{ field: string; message: string }>
|
|
286
|
+
) {
|
|
287
|
+
super(message, 'VALIDATION_ERROR', 400);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### TS-016: Narrow unknown errors
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
// BAD
|
|
296
|
+
try { ... } catch (e) {
|
|
297
|
+
console.log(e.message); // e is unknown
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// GOOD
|
|
301
|
+
try { ... } catch (e) {
|
|
302
|
+
if (e instanceof Error) {
|
|
303
|
+
console.log(e.message);
|
|
304
|
+
} else {
|
|
305
|
+
console.log('Unknown error', e);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Or use a helper
|
|
310
|
+
function getErrorMessage(error: unknown): string {
|
|
311
|
+
if (error instanceof Error) return error.message;
|
|
312
|
+
if (typeof error === 'string') return error;
|
|
313
|
+
return 'Unknown error';
|
|
314
|
+
}
|
|
315
|
+
```
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# TypeScript Patterns
|
|
2
|
+
|
|
3
|
+
Common patterns for clean, type-safe code.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Utility Types
|
|
8
|
+
|
|
9
|
+
### PAT-001: Use built-in utility types
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// Partial - all properties optional
|
|
13
|
+
type UpdateUser = Partial<User>;
|
|
14
|
+
|
|
15
|
+
// Required - all properties required
|
|
16
|
+
type CompleteUser = Required<User>;
|
|
17
|
+
|
|
18
|
+
// Pick - subset of properties
|
|
19
|
+
type UserCredentials = Pick<User, 'email' | 'password'>;
|
|
20
|
+
|
|
21
|
+
// Omit - exclude properties
|
|
22
|
+
type PublicUser = Omit<User, 'password' | 'hashedPassword'>;
|
|
23
|
+
|
|
24
|
+
// Record - object with known keys
|
|
25
|
+
type StatusMap = Record<OrderStatus, string>;
|
|
26
|
+
|
|
27
|
+
// Readonly - immutable
|
|
28
|
+
type FrozenConfig = Readonly<Config>;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### PAT-002: Create custom utility types
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// Make specific keys optional
|
|
35
|
+
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
36
|
+
type CreateUser = PartialBy<User, 'id' | 'createdAt'>;
|
|
37
|
+
|
|
38
|
+
// Make specific keys required
|
|
39
|
+
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
|
|
40
|
+
type UserWithEmail = RequiredBy<PartialUser, 'email'>;
|
|
41
|
+
|
|
42
|
+
// Deep partial
|
|
43
|
+
type DeepPartial<T> = {
|
|
44
|
+
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
|
45
|
+
};
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### PAT-003: Extract types from existing code
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// From function return
|
|
52
|
+
type UserResult = Awaited<ReturnType<typeof getUser>>;
|
|
53
|
+
|
|
54
|
+
// From array element
|
|
55
|
+
type OrderItem = Order['items'][number];
|
|
56
|
+
|
|
57
|
+
// From object values
|
|
58
|
+
const STATUS = { pending: 1, active: 2, done: 3 } as const;
|
|
59
|
+
type StatusKey = keyof typeof STATUS; // 'pending' | 'active' | 'done'
|
|
60
|
+
type StatusValue = typeof STATUS[StatusKey]; // 1 | 2 | 3
|
|
61
|
+
|
|
62
|
+
// From Zod schema
|
|
63
|
+
type User = z.infer<typeof userSchema>;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Discriminated Unions
|
|
69
|
+
|
|
70
|
+
### PAT-004: Model state with discriminated unions
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// Instead of optional flags
|
|
74
|
+
// BAD
|
|
75
|
+
interface Request {
|
|
76
|
+
status: 'loading' | 'success' | 'error';
|
|
77
|
+
data?: User;
|
|
78
|
+
error?: Error;
|
|
79
|
+
isLoading?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// GOOD - discriminated union
|
|
83
|
+
type RequestState<T> =
|
|
84
|
+
| { status: 'idle' }
|
|
85
|
+
| { status: 'loading' }
|
|
86
|
+
| { status: 'success'; data: T }
|
|
87
|
+
| { status: 'error'; error: Error };
|
|
88
|
+
|
|
89
|
+
// Usage with exhaustive checking
|
|
90
|
+
function render(state: RequestState<User>) {
|
|
91
|
+
switch (state.status) {
|
|
92
|
+
case 'idle':
|
|
93
|
+
return <Placeholder />;
|
|
94
|
+
case 'loading':
|
|
95
|
+
return <Spinner />;
|
|
96
|
+
case 'success':
|
|
97
|
+
return <UserCard user={state.data} />;
|
|
98
|
+
case 'error':
|
|
99
|
+
return <Error message={state.error.message} />;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### PAT-005: Use never for exhaustive checks
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
function assertNever(value: never): never {
|
|
108
|
+
throw new Error(`Unexpected value: ${value}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };
|
|
112
|
+
|
|
113
|
+
function getArea(shape: Shape): number {
|
|
114
|
+
switch (shape.kind) {
|
|
115
|
+
case 'circle':
|
|
116
|
+
return Math.PI * shape.radius ** 2;
|
|
117
|
+
case 'square':
|
|
118
|
+
return shape.side ** 2;
|
|
119
|
+
default:
|
|
120
|
+
return assertNever(shape); // Compile error if new shape added
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Type Guards
|
|
128
|
+
|
|
129
|
+
### PAT-006: Create custom type guards
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// Simple type guard
|
|
133
|
+
function isString(value: unknown): value is string {
|
|
134
|
+
return typeof value === 'string';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Object type guard
|
|
138
|
+
function isUser(value: unknown): value is User {
|
|
139
|
+
return (
|
|
140
|
+
typeof value === 'object' &&
|
|
141
|
+
value !== null &&
|
|
142
|
+
'id' in value &&
|
|
143
|
+
'email' in value
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// With Zod (preferred)
|
|
148
|
+
function isUser(value: unknown): value is User {
|
|
149
|
+
return userSchema.safeParse(value).success;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Array type guard
|
|
153
|
+
function isStringArray(value: unknown): value is string[] {
|
|
154
|
+
return Array.isArray(value) && value.every(isString);
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### PAT-007: Use assertion functions
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
function assertDefined<T>(
|
|
162
|
+
value: T | undefined | null,
|
|
163
|
+
message?: string
|
|
164
|
+
): asserts value is T {
|
|
165
|
+
if (value === undefined || value === null) {
|
|
166
|
+
throw new Error(message ?? 'Value is not defined');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Usage
|
|
171
|
+
const user = users.find(u => u.id === id);
|
|
172
|
+
assertDefined(user, `User ${id} not found`);
|
|
173
|
+
// user is now User (not User | undefined)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Generics
|
|
179
|
+
|
|
180
|
+
### PAT-008: Use generics for reusable types
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// Generic response wrapper
|
|
184
|
+
interface ApiResponse<T> {
|
|
185
|
+
data: T;
|
|
186
|
+
timestamp: Date;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Generic result type
|
|
190
|
+
type Result<T, E = Error> =
|
|
191
|
+
| { ok: true; value: T }
|
|
192
|
+
| { ok: false; error: E };
|
|
193
|
+
|
|
194
|
+
// Generic with constraints
|
|
195
|
+
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
|
|
196
|
+
return obj[key];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Generic with default
|
|
200
|
+
interface Paginated<T, Meta = { page: number; total: number }> {
|
|
201
|
+
items: T[];
|
|
202
|
+
meta: Meta;
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### PAT-009: Constrain generics appropriately
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// Require specific shape
|
|
210
|
+
function clone<T extends object>(obj: T): T {
|
|
211
|
+
return { ...obj };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Require specific properties
|
|
215
|
+
function getId<T extends { id: string }>(item: T): string {
|
|
216
|
+
return item.id;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Multiple constraints
|
|
220
|
+
function merge<T extends object, U extends object>(a: T, b: U): T & U {
|
|
221
|
+
return { ...a, ...b };
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Async Patterns
|
|
228
|
+
|
|
229
|
+
### PAT-010: Type async iterators
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
async function* paginate<T>(
|
|
233
|
+
fetcher: (page: number) => Promise<{ items: T[]; hasMore: boolean }>
|
|
234
|
+
): AsyncGenerator<T[], void, unknown> {
|
|
235
|
+
let page = 1;
|
|
236
|
+
let hasMore = true;
|
|
237
|
+
|
|
238
|
+
while (hasMore) {
|
|
239
|
+
const result = await fetcher(page);
|
|
240
|
+
yield result.items;
|
|
241
|
+
hasMore = result.hasMore;
|
|
242
|
+
page++;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Usage
|
|
247
|
+
for await (const batch of paginate(fetchUsers)) {
|
|
248
|
+
await processUsers(batch);
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### PAT-011: Use AbortSignal for cancellation
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
async function fetchWithTimeout<T>(
|
|
256
|
+
url: string,
|
|
257
|
+
options: { timeout: number; signal?: AbortSignal }
|
|
258
|
+
): Promise<T> {
|
|
259
|
+
const controller = new AbortController();
|
|
260
|
+
const timeoutId = setTimeout(() => controller.abort(), options.timeout);
|
|
261
|
+
|
|
262
|
+
// Combine signals
|
|
263
|
+
options.signal?.addEventListener('abort', () => controller.abort());
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
267
|
+
return response.json();
|
|
268
|
+
} finally {
|
|
269
|
+
clearTimeout(timeoutId);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Object Patterns
|
|
277
|
+
|
|
278
|
+
### PAT-012: Use satisfies for type checking with inference
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// satisfies checks type but preserves literal types
|
|
282
|
+
const config = {
|
|
283
|
+
port: 3000,
|
|
284
|
+
host: 'localhost',
|
|
285
|
+
debug: true,
|
|
286
|
+
} satisfies Config;
|
|
287
|
+
|
|
288
|
+
// config.port is number, not widened
|
|
289
|
+
// But still validated against Config shape
|
|
290
|
+
|
|
291
|
+
// Useful for route definitions
|
|
292
|
+
const routes = {
|
|
293
|
+
home: '/',
|
|
294
|
+
users: '/users',
|
|
295
|
+
userDetail: '/users/:id',
|
|
296
|
+
} satisfies Record<string, string>;
|
|
297
|
+
|
|
298
|
+
// routes.home is '/', not string
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### PAT-013: Immutable updates
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
// Spread for shallow copies
|
|
305
|
+
const updated = { ...user, name: 'New Name' };
|
|
306
|
+
|
|
307
|
+
// For arrays
|
|
308
|
+
const added = [...items, newItem];
|
|
309
|
+
const removed = items.filter(i => i.id !== id);
|
|
310
|
+
const replaced = items.map(i => i.id === id ? updated : i);
|
|
311
|
+
|
|
312
|
+
// For nested updates
|
|
313
|
+
const nested = {
|
|
314
|
+
...state,
|
|
315
|
+
user: {
|
|
316
|
+
...state.user,
|
|
317
|
+
profile: {
|
|
318
|
+
...state.user.profile,
|
|
319
|
+
name: 'New Name',
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// Or use immer for complex updates
|
|
325
|
+
import { produce } from 'immer';
|
|
326
|
+
const nested = produce(state, draft => {
|
|
327
|
+
draft.user.profile.name = 'New Name';
|
|
328
|
+
});
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Module Patterns
|
|
334
|
+
|
|
335
|
+
### PAT-014: Use namespace for grouping
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
// Group related functions
|
|
339
|
+
export const User = {
|
|
340
|
+
create: (data: CreateUserData): User => ({ ... }),
|
|
341
|
+
validate: (user: unknown): user is User => userSchema.safeParse(user).success,
|
|
342
|
+
format: (user: User): string => `${user.name} <${user.email}>`,
|
|
343
|
+
} as const;
|
|
344
|
+
|
|
345
|
+
// Usage
|
|
346
|
+
const user = User.create({ name: 'John', email: 'john@example.com' });
|
|
347
|
+
if (User.validate(data)) { ... }
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### PAT-015: Builder pattern for complex objects
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
class QueryBuilder<T> {
|
|
354
|
+
private filters: Filter[] = [];
|
|
355
|
+
private sortBy?: string;
|
|
356
|
+
private limit?: number;
|
|
357
|
+
|
|
358
|
+
where(filter: Filter): this {
|
|
359
|
+
this.filters.push(filter);
|
|
360
|
+
return this;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
orderBy(field: keyof T): this {
|
|
364
|
+
this.sortBy = String(field);
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
take(count: number): this {
|
|
369
|
+
this.limit = count;
|
|
370
|
+
return this;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
build(): Query<T> {
|
|
374
|
+
return { filters: this.filters, sortBy: this.sortBy, limit: this.limit };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Usage
|
|
379
|
+
const query = new QueryBuilder<User>()
|
|
380
|
+
.where({ field: 'status', op: 'eq', value: 'active' })
|
|
381
|
+
.orderBy('createdAt')
|
|
382
|
+
.take(10)
|
|
383
|
+
.build();
|
|
384
|
+
```
|