@clipboard-health/ai-rules 1.2.0 → 1.2.2
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/backend/CLAUDE.md +1 -219
- package/common/CLAUDE.md +1 -61
- package/frontend/CLAUDE.md +1 -1037
- package/fullstack/CLAUDE.md +1 -1195
- package/package.json +1 -1
- package/scripts/constants.js +5 -1
- package/scripts/sync.js +1 -1
package/frontend/CLAUDE.md
CHANGED
|
@@ -1,1037 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
# Code style and structure
|
|
4
|
-
|
|
5
|
-
- Write concise, technical TypeScript code with accurate examples.
|
|
6
|
-
- Use functional and declarative programming patterns.
|
|
7
|
-
- Prefer iteration and modularization over code duplication.
|
|
8
|
-
- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
|
|
9
|
-
- Structure files: constants, types, exported functions, non-exported functions.
|
|
10
|
-
- Avoid magic strings and numbers; define constants.
|
|
11
|
-
- Use camelCase for files and directories (e.g., modules/shiftOffers.ts).
|
|
12
|
-
- When declaring functions, use the `function` keyword, not `const`.
|
|
13
|
-
- Files should read from top to bottom: `export`ed items live on top and the internal functions and methods they call go below them.
|
|
14
|
-
- Prefer data immutability.
|
|
15
|
-
|
|
16
|
-
# Commit messages
|
|
17
|
-
|
|
18
|
-
- Follow the Conventional Commits 1.0 spec for commit messages and in pull request titles.
|
|
19
|
-
|
|
20
|
-
<!-- Source: .ruler/common/errorHandlingAndValidation.md -->
|
|
21
|
-
|
|
22
|
-
# Error handling and validation
|
|
23
|
-
|
|
24
|
-
- Sanitize user input.
|
|
25
|
-
- Handle errors and edge cases at the beginning of functions.
|
|
26
|
-
- Use early returns for error conditions to avoid deeply nested if statements.
|
|
27
|
-
- Place the happy path last in the function for improved readability.
|
|
28
|
-
- Avoid unnecessary else statements; use the if-return pattern instead.
|
|
29
|
-
- Use guard clauses to handle preconditions and invalid states early.
|
|
30
|
-
- Implement proper error logging and user-friendly error messages.
|
|
31
|
-
- Favor `@clipboard-health/util-ts`'s `Either` type for expected errors instead of `try`/`catch`.
|
|
32
|
-
|
|
33
|
-
<!-- Source: .ruler/common/testing.md -->
|
|
34
|
-
|
|
35
|
-
# Testing
|
|
36
|
-
|
|
37
|
-
- Follow the Arrange-Act-Assert convention for tests with newlines between each section.
|
|
38
|
-
- Name test variables using the `mockX`, `input`, `expected`, `actual` convention.
|
|
39
|
-
- Aim for high test coverage, writing both positive and negative test cases.
|
|
40
|
-
- Prefer `it.each` for multiple test cases.
|
|
41
|
-
- Avoid conditional logic in tests.
|
|
42
|
-
|
|
43
|
-
<!-- Source: .ruler/common/typeScript.md -->
|
|
44
|
-
|
|
45
|
-
# TypeScript usage
|
|
46
|
-
|
|
47
|
-
- Use strict-mode TypeScript for all code; prefer interfaces over types.
|
|
48
|
-
- Avoid enums; use const maps instead.
|
|
49
|
-
- Strive for precise types. Look for type definitions in the codebase and create your own if none exist.
|
|
50
|
-
- Avoid using type assertions like `as` or `!` unless absolutely necessary.
|
|
51
|
-
- Use the `unknown` type instead of `any` when the type is truly unknown.
|
|
52
|
-
- Use an object to pass multiple function params and to return results.
|
|
53
|
-
- Leverage union types, intersection types, and conditional types for complex type definitions.
|
|
54
|
-
- Use mapped types and utility types (e.g., `Partial<T>`, `Pick<T>`, `Omit<T>`) to transform existing types.
|
|
55
|
-
- Implement generic types to create reusable, flexible type definitions.
|
|
56
|
-
- Utilize the `keyof` operator and index access types for dynamic property access.
|
|
57
|
-
- Implement discriminated unions for type-safe handling of different object shapes where appropriate.
|
|
58
|
-
- Use the `infer` keyword in conditional types for type inference.
|
|
59
|
-
- Leverage `readonly` properties for function parameter immutability.
|
|
60
|
-
- Prefer narrow types whenever possible with `as const` assertions, `typeof`, `instanceof`, `satisfies`, and custom type guards.
|
|
61
|
-
- Implement exhaustiveness checking using `never`.
|
|
62
|
-
|
|
63
|
-
<!-- Source: .ruler/frontend/custom-hooks.md -->
|
|
64
|
-
|
|
65
|
-
# Custom Hook Standards
|
|
66
|
-
|
|
67
|
-
## Hook Structure
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
export function useFeature(params: Params, options: UseFeatureOptions = {}) {
|
|
71
|
-
const query = useQuery(...);
|
|
72
|
-
const computed = useMemo(() => transformData(query.data), [query.data]);
|
|
73
|
-
const handleAction = useCallback(async () => { /* ... */ }, []);
|
|
74
|
-
|
|
75
|
-
return { data: computed, isLoading: query.isLoading, handleAction };
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## Naming Rules
|
|
80
|
-
|
|
81
|
-
- **Always** prefix with `use`
|
|
82
|
-
- Boolean: Use `useIs*`, `useHas*`, or `useCan*` (e.g., `useIsEnabled`, `useHasPermission`, `useCanEdit`)
|
|
83
|
-
- Data: `useGetUser`, `useFeatureData`
|
|
84
|
-
- Actions: `useSubmitForm`
|
|
85
|
-
|
|
86
|
-
## Return Values
|
|
87
|
-
|
|
88
|
-
- Return objects (not arrays) for complex hooks
|
|
89
|
-
- Name clearly: `isLoading` not `loading`
|
|
90
|
-
|
|
91
|
-
## State Management with Constate
|
|
92
|
-
|
|
93
|
-
Use `constate` for shared state between components:
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
import constate from "constate";
|
|
97
|
-
|
|
98
|
-
function useFilters() {
|
|
99
|
-
const [filters, setFilters] = useState<Filters>({});
|
|
100
|
-
return { filters, setFilters };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export const [FiltersProvider, useFiltersContext] = constate(useFilters);
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
**When to use:** Sharing state between siblings, feature-level state
|
|
107
|
-
**When NOT:** Server state (use React Query), simple parent-child (use props)
|
|
108
|
-
|
|
109
|
-
## Hook Patterns
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
// Boolean hooks
|
|
113
|
-
export function useIsFeatureEnabled(): boolean {
|
|
114
|
-
return useFeatureFlags().includes("feature");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Data transformation
|
|
118
|
-
export function useTransformedData() {
|
|
119
|
-
const { data, isLoading } = useGetRawData();
|
|
120
|
-
const transformed = useMemo(() => data?.map(format), [data]);
|
|
121
|
-
return { data: transformed, isLoading };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Composite hooks
|
|
125
|
-
export function useBookingsData() {
|
|
126
|
-
const shifts = useGetShifts();
|
|
127
|
-
const invites = useGetInvites();
|
|
128
|
-
|
|
129
|
-
const combined = useMemo(
|
|
130
|
-
() => [...(shifts.data ?? []), ...(invites.data ?? [])],
|
|
131
|
-
[shifts.data, invites.data],
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
return { data: combined, isLoading: shifts.isLoading || invites.isLoading };
|
|
135
|
-
}
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
## Best Practices
|
|
139
|
-
|
|
140
|
-
- Use options object for flexibility
|
|
141
|
-
- Be explicit about dependencies
|
|
142
|
-
- API hooks in `api/`, logic hooks in `hooks/`
|
|
143
|
-
- Return stable references with `useCallback`/`useMemo`
|
|
144
|
-
|
|
145
|
-
<!-- Source: .ruler/frontend/data-fetching.md -->
|
|
146
|
-
|
|
147
|
-
# Data Fetching Standards
|
|
148
|
-
|
|
149
|
-
## Technology Stack
|
|
150
|
-
|
|
151
|
-
- **React Query** (@tanstack/react-query) for all API calls
|
|
152
|
-
- **Zod** for response validation
|
|
153
|
-
|
|
154
|
-
## Core Principles
|
|
155
|
-
|
|
156
|
-
1. **Use URL and query parameters in query keys** - Makes cache invalidation predictable
|
|
157
|
-
2. **Define Zod schemas** for all API requests/responses
|
|
158
|
-
3. **Rely on React Query state** - Use `isLoading`, `isError`, `isSuccess`
|
|
159
|
-
4. **Use `enabled` for conditional fetching**
|
|
160
|
-
5. **Use `invalidateQueries` for disabled queries** - Not `refetch()` which ignores enabled state
|
|
161
|
-
|
|
162
|
-
## Hook Patterns
|
|
163
|
-
|
|
164
|
-
```typescript
|
|
165
|
-
// Simple query
|
|
166
|
-
export function useGetUser(userId: string) {
|
|
167
|
-
return useQuery({
|
|
168
|
-
queryKey: ["users", userId],
|
|
169
|
-
queryFn: () => api.get(`/users/${userId}`),
|
|
170
|
-
responseSchema: userSchema,
|
|
171
|
-
enabled: !!userId,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Paginated query
|
|
176
|
-
export function usePaginatedItems(params: Params) {
|
|
177
|
-
return useInfiniteQuery({
|
|
178
|
-
queryKey: ["items", params],
|
|
179
|
-
queryFn: ({ pageParam }) => api.get("/items", { cursor: pageParam, ...params }),
|
|
180
|
-
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Mutations
|
|
185
|
-
export function useCreateItem() {
|
|
186
|
-
const queryClient = useQueryClient();
|
|
187
|
-
return useMutation({
|
|
188
|
-
mutationFn: (data: CreateItemRequest) => api.post("/items", data),
|
|
189
|
-
onSuccess: () => queryClient.invalidateQueries(["items"]),
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
## Error Handling
|
|
195
|
-
|
|
196
|
-
For detailed error handling strategies, see `error-handling.md`.
|
|
197
|
-
|
|
198
|
-
```typescript
|
|
199
|
-
useQuery({
|
|
200
|
-
queryKey: ["resource"],
|
|
201
|
-
queryFn: fetchResource,
|
|
202
|
-
useErrorBoundary: (error) => {
|
|
203
|
-
// Show error boundary for 500s, not 404s
|
|
204
|
-
return !(axios.isAxiosError(error) && error.response?.status === 404);
|
|
205
|
-
},
|
|
206
|
-
retry: (failureCount, error) => {
|
|
207
|
-
// Don't retry 4xx errors
|
|
208
|
-
if (axios.isAxiosError(error) && error.response?.status < 500) return false;
|
|
209
|
-
return failureCount < 3;
|
|
210
|
-
},
|
|
211
|
-
});
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
## Query Keys
|
|
215
|
-
|
|
216
|
-
```typescript
|
|
217
|
-
// Include URL and params for predictable cache invalidation
|
|
218
|
-
export const queryKeys = {
|
|
219
|
-
users: ["users"] as const,
|
|
220
|
-
user: (id: string) => ["users", id] as const,
|
|
221
|
-
userPosts: (id: string) => ["users", id, "posts"] as const,
|
|
222
|
-
};
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
## Conditional Fetching
|
|
226
|
-
|
|
227
|
-
```typescript
|
|
228
|
-
const { data } = useQuery({
|
|
229
|
-
queryKey: ["resource", id],
|
|
230
|
-
queryFn: () => fetchResource(id),
|
|
231
|
-
enabled: !!id, // Only fetch when id exists
|
|
232
|
-
});
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
## Naming Conventions
|
|
236
|
-
|
|
237
|
-
- `useGetX` - Simple queries
|
|
238
|
-
- `usePaginatedX` - Infinite queries
|
|
239
|
-
- `useCreateX`, `useUpdateX`, `useDeleteX` - Mutations
|
|
240
|
-
|
|
241
|
-
## Hook Location
|
|
242
|
-
|
|
243
|
-
Place in `api/` folder within feature directory. One endpoint = one hook.
|
|
244
|
-
|
|
245
|
-
<!-- Source: .ruler/frontend/error-handling.md -->
|
|
246
|
-
|
|
247
|
-
# Error Handling Standards
|
|
248
|
-
|
|
249
|
-
## React Query Error Handling
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
useQuery({
|
|
253
|
-
queryKey: ["resource"],
|
|
254
|
-
queryFn: fetchResource,
|
|
255
|
-
useErrorBoundary: (error) => {
|
|
256
|
-
// Show error boundary for 500s, not 404s
|
|
257
|
-
return !(axios.isAxiosError(error) && error.response?.status === 404);
|
|
258
|
-
},
|
|
259
|
-
retry: (failureCount, error) => {
|
|
260
|
-
// Don't retry 4xx errors
|
|
261
|
-
if (axios.isAxiosError(error) && error.response?.status < 500) return false;
|
|
262
|
-
return failureCount < 3;
|
|
263
|
-
},
|
|
264
|
-
});
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
## Component Error States
|
|
268
|
-
|
|
269
|
-
```typescript
|
|
270
|
-
export function DataComponent() {
|
|
271
|
-
const { data, isLoading, isError, refetch } = useGetData();
|
|
272
|
-
|
|
273
|
-
if (isLoading) return <LoadingState />;
|
|
274
|
-
if (isError) return <ErrorState onRetry={refetch} />;
|
|
275
|
-
|
|
276
|
-
return <DataDisplay data={data} />;
|
|
277
|
-
}
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
## Mutation Error Handling
|
|
281
|
-
|
|
282
|
-
```typescript
|
|
283
|
-
export function useCreateDocument() {
|
|
284
|
-
return useMutation({
|
|
285
|
-
mutationFn: createDocumentApi,
|
|
286
|
-
onSuccess: () => {
|
|
287
|
-
queryClient.invalidateQueries(["documents"]);
|
|
288
|
-
showSuccessToast("Document created");
|
|
289
|
-
},
|
|
290
|
-
onError: (error) => {
|
|
291
|
-
logError("CREATE_DOCUMENT_FAILURE", error);
|
|
292
|
-
showErrorToast("Failed to create document");
|
|
293
|
-
},
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
## Validation Errors
|
|
299
|
-
|
|
300
|
-
```typescript
|
|
301
|
-
// Zod validation
|
|
302
|
-
const formSchema = z.object({
|
|
303
|
-
email: z.string().email("Invalid email"),
|
|
304
|
-
age: z.number().min(18, "Must be 18+"),
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
try {
|
|
308
|
-
const validated = formSchema.parse(formData);
|
|
309
|
-
} catch (error) {
|
|
310
|
-
if (error instanceof z.ZodError) {
|
|
311
|
-
error.errors.forEach((err) => showFieldError(err.path.join("."), err.message));
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
## Error Boundaries
|
|
317
|
-
|
|
318
|
-
```typescript
|
|
319
|
-
export class ErrorBoundary extends Component<Props, State> {
|
|
320
|
-
state = { hasError: false };
|
|
321
|
-
|
|
322
|
-
static getDerivedStateFromError(error: Error) {
|
|
323
|
-
return { hasError: true, error };
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
327
|
-
logEvent("ERROR_BOUNDARY", { error: error.message });
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
render() {
|
|
331
|
-
return this.state.hasError ? <ErrorFallback /> : this.props.children;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
## Best Practices
|
|
337
|
-
|
|
338
|
-
- **Always handle errors** - Check `isError` state
|
|
339
|
-
- **User-friendly messages** - Don't show technical errors
|
|
340
|
-
- **Log for debugging** - Include context
|
|
341
|
-
- **Provide recovery actions** - Retry, dismiss, navigate
|
|
342
|
-
- **Different strategies** - Error boundary vs inline vs retry
|
|
343
|
-
|
|
344
|
-
<!-- Source: .ruler/frontend/file-organization.md -->
|
|
345
|
-
|
|
346
|
-
# File Organization Standards
|
|
347
|
-
|
|
348
|
-
## Feature-based Structure
|
|
349
|
-
|
|
350
|
-
```text
|
|
351
|
-
FeatureName/
|
|
352
|
-
├── api/ # Data fetching hooks
|
|
353
|
-
│ ├── useGetFeature.ts
|
|
354
|
-
│ ├── useUpdateFeature.ts
|
|
355
|
-
│ └── useDeleteFeature.ts
|
|
356
|
-
├── components/ # Feature-specific components
|
|
357
|
-
│ ├── FeatureCard.tsx
|
|
358
|
-
│ ├── FeatureList.tsx
|
|
359
|
-
│ └── FeatureHeader.tsx
|
|
360
|
-
├── hooks/ # Feature-specific hooks (non-API)
|
|
361
|
-
│ ├── useFeatureLogic.ts
|
|
362
|
-
│ └── useFeatureState.ts
|
|
363
|
-
├── utils/ # Feature utilities
|
|
364
|
-
│ ├── formatFeature.ts
|
|
365
|
-
│ ├── formatFeature.test.ts
|
|
366
|
-
│ └── validateFeature.ts
|
|
367
|
-
├── __tests__/ # Integration tests (optional)
|
|
368
|
-
│ └── FeatureFlow.test.tsx
|
|
369
|
-
├── Page.tsx # Main page component
|
|
370
|
-
├── Router.tsx # Feature routes
|
|
371
|
-
├── paths.ts # Route paths
|
|
372
|
-
├── types.ts # Shared types
|
|
373
|
-
├── constants.ts # Constants
|
|
374
|
-
└── README.md # Feature documentation (optional)
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
## Naming Conventions
|
|
378
|
-
|
|
379
|
-
- Components: `PascalCase.tsx` (`UserProfile.tsx`)
|
|
380
|
-
- Hooks: `camelCase.ts` (`useUserProfile.ts`)
|
|
381
|
-
- Utils: `camelCase.ts` (`formatDate.ts`)
|
|
382
|
-
- Types: `camelCase.types.ts` (`user.types.ts`)
|
|
383
|
-
- Tests: `ComponentName.test.tsx`
|
|
384
|
-
|
|
385
|
-
<!-- Source: .ruler/frontend/react-patterns.md -->
|
|
386
|
-
|
|
387
|
-
# React Component Patterns
|
|
388
|
-
|
|
389
|
-
## Component Structure
|
|
390
|
-
|
|
391
|
-
```typescript
|
|
392
|
-
interface Props {
|
|
393
|
-
userId: string;
|
|
394
|
-
onUpdate?: (user: User) => void;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
export function UserProfile({ userId, onUpdate }: Props) {
|
|
398
|
-
// 1. Hooks
|
|
399
|
-
const { data, isLoading, error } = useGetUser(userId);
|
|
400
|
-
const [isEditing, setIsEditing] = useState(false);
|
|
401
|
-
|
|
402
|
-
// 2. Derived state (avoid useMemo for inexpensive operations)
|
|
403
|
-
const displayName = formatName(data);
|
|
404
|
-
|
|
405
|
-
// 3. Event handlers
|
|
406
|
-
const handleSave = useCallback(async () => {
|
|
407
|
-
await saveUser(data);
|
|
408
|
-
onUpdate?.(data);
|
|
409
|
-
}, [data, onUpdate]);
|
|
410
|
-
|
|
411
|
-
// 4. Early returns
|
|
412
|
-
if (isLoading) return <Loading />;
|
|
413
|
-
if (error) return <Error message={error.message} />;
|
|
414
|
-
if (!data) return <NotFound />;
|
|
415
|
-
|
|
416
|
-
// 5. Main render
|
|
417
|
-
return (
|
|
418
|
-
<Card>
|
|
419
|
-
<Typography>{displayName}</Typography>
|
|
420
|
-
<Button onClick={handleSave}>Save</Button>
|
|
421
|
-
</Card>
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
## Naming Conventions
|
|
427
|
-
|
|
428
|
-
- Components: `PascalCase` (`UserProfile`)
|
|
429
|
-
- Props interface: `Props` (co-located with component) or `ComponentNameProps` (exported/shared)
|
|
430
|
-
- Event handlers: `handle*` (`handleClick`, `handleSubmit`)
|
|
431
|
-
- Boolean props: `is*`, `has*`, `should*`
|
|
432
|
-
|
|
433
|
-
## Props Patterns
|
|
434
|
-
|
|
435
|
-
```typescript
|
|
436
|
-
// Simple props
|
|
437
|
-
interface Props {
|
|
438
|
-
title: string;
|
|
439
|
-
count: number;
|
|
440
|
-
onAction: () => void;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Discriminated unions for variants
|
|
444
|
-
type ButtonProps = { variant: "link"; href: string } | { variant: "button"; onClick: () => void };
|
|
445
|
-
|
|
446
|
-
// Optional callbacks
|
|
447
|
-
interface Props {
|
|
448
|
-
onSuccess?: (data: Data) => void;
|
|
449
|
-
onError?: (error: Error) => void;
|
|
450
|
-
}
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
## Composition Patterns
|
|
454
|
-
|
|
455
|
-
```typescript
|
|
456
|
-
// Container/Presentational
|
|
457
|
-
export function UserListContainer() {
|
|
458
|
-
const { data, isLoading, error } = useUsers();
|
|
459
|
-
if (isLoading) return <Loading />;
|
|
460
|
-
if (error) return <Error />;
|
|
461
|
-
return <UserList users={data} />;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Compound components
|
|
465
|
-
<Card>
|
|
466
|
-
<Card.Header title="User" />
|
|
467
|
-
<Card.Body>Content</Card.Body>
|
|
468
|
-
<Card.Actions>
|
|
469
|
-
<Button>Save</Button>
|
|
470
|
-
</Card.Actions>
|
|
471
|
-
</Card>
|
|
472
|
-
|
|
473
|
-
// Render props
|
|
474
|
-
<DataProvider>
|
|
475
|
-
{({ data, isLoading }) => (
|
|
476
|
-
isLoading ? <Loading /> : <Display data={data} />
|
|
477
|
-
)}
|
|
478
|
-
</DataProvider>
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
## Children Patterns
|
|
482
|
-
|
|
483
|
-
```typescript
|
|
484
|
-
// Typed children
|
|
485
|
-
interface Props {
|
|
486
|
-
children: ReactNode;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Render prop pattern
|
|
490
|
-
interface Props {
|
|
491
|
-
children: (data: Data) => ReactNode;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Element restrictions
|
|
495
|
-
interface Props {
|
|
496
|
-
children: ReactElement<ButtonProps>;
|
|
497
|
-
}
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
## List Rendering
|
|
501
|
-
|
|
502
|
-
```typescript
|
|
503
|
-
// ✅ Proper keys
|
|
504
|
-
{users.map((user) => <UserCard key={user.id} user={user} />)}
|
|
505
|
-
|
|
506
|
-
// ✅ Empty states
|
|
507
|
-
{users.length === 0 ? <EmptyState /> : users.map(...)}
|
|
508
|
-
|
|
509
|
-
// ❌ Never use index as key when list can change
|
|
510
|
-
{items.map((item, index) => <Item key={index} />)} // Wrong!
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
## Conditional Rendering
|
|
514
|
-
|
|
515
|
-
```typescript
|
|
516
|
-
// Simple conditional
|
|
517
|
-
{isLoading && <Spinner />}
|
|
518
|
-
|
|
519
|
-
// If-else
|
|
520
|
-
{isError ? <Error /> : <Success />}
|
|
521
|
-
|
|
522
|
-
// Multiple conditions
|
|
523
|
-
{isLoading ? <Loading /> : isError ? <Error /> : <Data />}
|
|
524
|
-
|
|
525
|
-
// With early returns (preferred for complex logic)
|
|
526
|
-
if (isLoading) return <Loading />;
|
|
527
|
-
if (isError) return <Error />;
|
|
528
|
-
return <Data />;
|
|
529
|
-
```
|
|
530
|
-
|
|
531
|
-
## Performance
|
|
532
|
-
|
|
533
|
-
```typescript
|
|
534
|
-
// Memoize expensive components
|
|
535
|
-
const MemoItem = memo(({ item }: Props) => <div>{item.name}</div>);
|
|
536
|
-
|
|
537
|
-
// Split state to avoid re-renders
|
|
538
|
-
function Parent() {
|
|
539
|
-
const [count, setCount] = useState(0);
|
|
540
|
-
const [text, setText] = useState("");
|
|
541
|
-
|
|
542
|
-
// Only CountDisplay re-renders when count changes
|
|
543
|
-
return (
|
|
544
|
-
<>
|
|
545
|
-
<CountDisplay count={count} />
|
|
546
|
-
<TextInput value={text} onChange={setText} />
|
|
547
|
-
</>
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
## Best Practices
|
|
553
|
-
|
|
554
|
-
- **Single Responsibility** - One component, one purpose
|
|
555
|
-
- **Composition over Props** - Use children and compound components
|
|
556
|
-
- **Colocate State** - Keep state close to where it's used
|
|
557
|
-
- **Type Everything** - Full TypeScript coverage
|
|
558
|
-
- **Test Behavior** - Test user interactions, not implementation
|
|
559
|
-
|
|
560
|
-
<!-- Source: .ruler/frontend/react.md -->
|
|
561
|
-
|
|
562
|
-
# React
|
|
563
|
-
|
|
564
|
-
- Destructure props in function body (improves readability and prop documentation)
|
|
565
|
-
- Prefer inline JSX over extracted variables
|
|
566
|
-
- Use custom hooks to encapsulate and reuse stateful logic
|
|
567
|
-
- Use Zod for request/response schemas in data-fetching hooks
|
|
568
|
-
- Use react-hook-form with Zod resolver for forms
|
|
569
|
-
- Use date-fns for date operations
|
|
570
|
-
|
|
571
|
-
<!-- Source: .ruler/frontend/styling.md -->
|
|
572
|
-
|
|
573
|
-
# Styling Standards
|
|
574
|
-
|
|
575
|
-
## Core Principles
|
|
576
|
-
|
|
577
|
-
1. **Always use `sx` prop** - Never CSS/SCSS/SASS files
|
|
578
|
-
2. **Use theme tokens** - Never hardcode colors/spacing
|
|
579
|
-
3. **Type-safe theme access** - Use `sx={(theme) => ({...})}`
|
|
580
|
-
4. **Use semantic names** - `theme.palette.text.primary`, not `common.white`
|
|
581
|
-
5. **Check Storybook first** - It's the single source of truth
|
|
582
|
-
|
|
583
|
-
## Restricted Patterns
|
|
584
|
-
|
|
585
|
-
❌ **DO NOT USE:**
|
|
586
|
-
|
|
587
|
-
- `styled()` or `makeStyles()` from MUI
|
|
588
|
-
- CSS/SCSS/SASS files
|
|
589
|
-
- Inline `style` prop (use `sx` instead)
|
|
590
|
-
- String paths for theme (`"text.secondary"` - not type-safe)
|
|
591
|
-
|
|
592
|
-
## Type-Safe Theme Access
|
|
593
|
-
|
|
594
|
-
```typescript
|
|
595
|
-
// ✅ Always use function form for type safety
|
|
596
|
-
<Box sx={(theme) => ({
|
|
597
|
-
backgroundColor: theme.palette.background.primary,
|
|
598
|
-
color: theme.palette.text.secondary,
|
|
599
|
-
padding: theme.spacing(4), // or just: 4
|
|
600
|
-
})} />
|
|
601
|
-
|
|
602
|
-
// ❌ Never hardcode
|
|
603
|
-
<Box sx={{
|
|
604
|
-
backgroundColor: "red", // ❌ Raw color
|
|
605
|
-
padding: "16px", // ❌ Raw size
|
|
606
|
-
color: "text.secondary", // ❌ String path
|
|
607
|
-
}} />
|
|
608
|
-
```
|
|
609
|
-
|
|
610
|
-
## Spacing System
|
|
611
|
-
|
|
612
|
-
Use indices 1-12 (4px-64px):
|
|
613
|
-
|
|
614
|
-
```typescript
|
|
615
|
-
<Box sx={{ padding: 5 }} /> // → 16px
|
|
616
|
-
<Box sx={{ marginX: 4 }} /> // → 12px left and right
|
|
617
|
-
<Box sx={{ gap: 3 }} /> // → 8px
|
|
618
|
-
```
|
|
619
|
-
|
|
620
|
-
**Use `rem` for fonts/heights (scales with user zoom), `px` for spacing:**
|
|
621
|
-
|
|
622
|
-
```typescript
|
|
623
|
-
<Box sx={(theme) => ({
|
|
624
|
-
height: "3rem", // ✅ Scales
|
|
625
|
-
fontSize: theme.typography.body1.fontSize,
|
|
626
|
-
padding: 5, // ✅ Prevents overflow
|
|
627
|
-
})} />
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
## Responsive Styles
|
|
631
|
-
|
|
632
|
-
```typescript
|
|
633
|
-
<Box sx={{
|
|
634
|
-
width: { xs: "100%", md: "50%" },
|
|
635
|
-
padding: { xs: 1, md: 3 },
|
|
636
|
-
}} />
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
## Pseudo-classes
|
|
640
|
-
|
|
641
|
-
```typescript
|
|
642
|
-
<Box sx={(theme) => ({
|
|
643
|
-
"&:hover": { backgroundColor: theme.palette.primary.dark },
|
|
644
|
-
"&:disabled": { opacity: 0.5 },
|
|
645
|
-
"& .child": { color: theme.palette.text.secondary },
|
|
646
|
-
})} />
|
|
647
|
-
```
|
|
648
|
-
|
|
649
|
-
## Shorthand Properties
|
|
650
|
-
|
|
651
|
-
✅ Use full names: `padding`, `paddingX`, `marginY`
|
|
652
|
-
❌ Avoid abbreviations: `p`, `px`, `my`
|
|
653
|
-
|
|
654
|
-
## Layout Components
|
|
655
|
-
|
|
656
|
-
Safe to import directly from MUI:
|
|
657
|
-
|
|
658
|
-
```typescript
|
|
659
|
-
import { Box, Stack, Container, Grid } from "@mui/material";
|
|
660
|
-
```
|
|
661
|
-
|
|
662
|
-
## Best Practices
|
|
663
|
-
|
|
664
|
-
- Type-safe access with `sx={(theme) => ({...})}`
|
|
665
|
-
- Use semantic token names
|
|
666
|
-
- Full property names, not abbreviations
|
|
667
|
-
|
|
668
|
-
<!-- Source: .ruler/frontend/testing.md -->
|
|
669
|
-
|
|
670
|
-
# Testing Standards
|
|
671
|
-
|
|
672
|
-
## Testing Trophy Philosophy
|
|
673
|
-
|
|
674
|
-
Focus on **Integration tests** - test how components work together as users experience them.
|
|
675
|
-
|
|
676
|
-
**Investment Priority:**
|
|
677
|
-
|
|
678
|
-
1. **Static** (TypeScript/ESLint) - Free confidence
|
|
679
|
-
2. **Integration** - Most valuable, test features not isolated components
|
|
680
|
-
3. **Unit** - For pure helpers/utilities only
|
|
681
|
-
4. **E2E** - Critical flows only
|
|
682
|
-
|
|
683
|
-
**Key Principle:** Test as close to how users interact with your app as possible.
|
|
684
|
-
|
|
685
|
-
## Technology Stack
|
|
686
|
-
|
|
687
|
-
- **Vitest** for test runner
|
|
688
|
-
- **@testing-library/react** for component testing
|
|
689
|
-
- **@testing-library/user-event** for user interactions
|
|
690
|
-
- **Mock Service Worker (MSW)** for API mocking
|
|
691
|
-
|
|
692
|
-
## Test Structure
|
|
693
|
-
|
|
694
|
-
```typescript
|
|
695
|
-
describe("ComponentName", () => {
|
|
696
|
-
it("should render correctly", () => {
|
|
697
|
-
render(<Component />);
|
|
698
|
-
expect(screen.getByText("Expected")).toBeInTheDocument();
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
it("should handle user interaction", async () => {
|
|
702
|
-
const user = userEvent.setup();
|
|
703
|
-
render(<Component />);
|
|
704
|
-
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
705
|
-
await waitFor(() => expect(screen.getByText("Success")).toBeInTheDocument());
|
|
706
|
-
});
|
|
707
|
-
});
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
## Test Naming
|
|
711
|
-
|
|
712
|
-
Pattern: `should [behavior] when [condition]`
|
|
713
|
-
|
|
714
|
-
## Querying Elements - Priority Order
|
|
715
|
-
|
|
716
|
-
1. `getByRole` - Best for accessibility
|
|
717
|
-
2. `getByLabelText` - For form fields
|
|
718
|
-
3. `getByText` - For non-interactive content
|
|
719
|
-
4. `getByTestId` - ⚠️ **LAST RESORT**
|
|
720
|
-
|
|
721
|
-
```typescript
|
|
722
|
-
// ✅ Prefer
|
|
723
|
-
screen.getByRole("button", { name: /submit/i });
|
|
724
|
-
screen.getByLabelText("Email");
|
|
725
|
-
|
|
726
|
-
// ❌ Avoid
|
|
727
|
-
screen.getByClassName("user-card");
|
|
728
|
-
screen.getByTestId("element");
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
## User Interactions
|
|
732
|
-
|
|
733
|
-
```typescript
|
|
734
|
-
it("should handle form submission", async () => {
|
|
735
|
-
const user = userEvent.setup();
|
|
736
|
-
const onSubmit = vi.fn();
|
|
737
|
-
render(<Form onSubmit={onSubmit} />);
|
|
738
|
-
|
|
739
|
-
await user.type(screen.getByLabelText("Name"), "John");
|
|
740
|
-
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
741
|
-
|
|
742
|
-
expect(onSubmit).toHaveBeenCalledWith({ name: "John" });
|
|
743
|
-
});
|
|
744
|
-
```
|
|
745
|
-
|
|
746
|
-
## Hook Testing
|
|
747
|
-
|
|
748
|
-
```typescript
|
|
749
|
-
describe("useCustomHook", () => {
|
|
750
|
-
it("should return data after loading", async () => {
|
|
751
|
-
const { result } = renderHook(() => useCustomHook());
|
|
752
|
-
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
753
|
-
expect(result.current.data).toEqual(expectedData);
|
|
754
|
-
});
|
|
755
|
-
});
|
|
756
|
-
```
|
|
757
|
-
|
|
758
|
-
## MSW Pattern
|
|
759
|
-
|
|
760
|
-
**Always export factory functions, not static handlers:**
|
|
761
|
-
|
|
762
|
-
```typescript
|
|
763
|
-
// ✅ Good - flexible per test
|
|
764
|
-
export const createTestHandler = (data: Data[]) =>
|
|
765
|
-
rest.get(`/api/resource`, (req, res, ctx) => res(ctx.json(data)));
|
|
766
|
-
|
|
767
|
-
// Usage
|
|
768
|
-
mockServer.use(createTestHandler(customData));
|
|
769
|
-
```
|
|
770
|
-
|
|
771
|
-
## Mocking
|
|
772
|
-
|
|
773
|
-
```typescript
|
|
774
|
-
vi.mock("@/lib/api", () => ({
|
|
775
|
-
get: vi.fn().mockResolvedValue({ data: { id: "1" } }),
|
|
776
|
-
}));
|
|
777
|
-
```
|
|
778
|
-
|
|
779
|
-
## What to Test
|
|
780
|
-
|
|
781
|
-
**✅ Do Test:**
|
|
782
|
-
|
|
783
|
-
- Integration tests for features
|
|
784
|
-
- Unit tests for utilities
|
|
785
|
-
- All states (loading, success, error)
|
|
786
|
-
- User interactions
|
|
787
|
-
|
|
788
|
-
**❌ Don't Test:**
|
|
789
|
-
|
|
790
|
-
- Implementation details
|
|
791
|
-
- Third-party libraries
|
|
792
|
-
- Styles/CSS
|
|
793
|
-
|
|
794
|
-
<!-- Source: .ruler/frontend/typeScript.md -->
|
|
795
|
-
|
|
796
|
-
# TypeScript Standards
|
|
797
|
-
|
|
798
|
-
## Core Principles
|
|
799
|
-
|
|
800
|
-
- **Strict mode enabled** - Avoid `any`
|
|
801
|
-
- **Prefer type inference** - Let TypeScript infer when possible
|
|
802
|
-
- **Explicit return types** for exported functions
|
|
803
|
-
- **Zod for runtime validation** - Single source of truth
|
|
804
|
-
- **No type assertions** unless unavoidable
|
|
805
|
-
|
|
806
|
-
## Type vs Interface
|
|
807
|
-
|
|
808
|
-
```typescript
|
|
809
|
-
// ✅ Use interface for: props, object shapes, extensible types
|
|
810
|
-
interface UserCardProps {
|
|
811
|
-
user: User;
|
|
812
|
-
onSelect: (id: string) => void;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
interface BaseEntity {
|
|
816
|
-
id: string;
|
|
817
|
-
createdAt: string;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
interface User extends BaseEntity {
|
|
821
|
-
name: string;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// ✅ Use type for: unions, intersections, tuples, derived types
|
|
825
|
-
type Status = "pending" | "active" | "completed";
|
|
826
|
-
type UserWithRoles = User & { roles: string[] };
|
|
827
|
-
type Coordinate = [number, number];
|
|
828
|
-
type UserKeys = keyof User;
|
|
829
|
-
```
|
|
830
|
-
|
|
831
|
-
## Naming Conventions
|
|
832
|
-
|
|
833
|
-
```typescript
|
|
834
|
-
// ✅ Suffix with purpose (no I or T prefix)
|
|
835
|
-
interface ButtonProps { ... }
|
|
836
|
-
type ApiResponse = { ... };
|
|
837
|
-
type UserOptions = { ... };
|
|
838
|
-
|
|
839
|
-
// ✅ Boolean properties
|
|
840
|
-
interface User {
|
|
841
|
-
isActive: boolean;
|
|
842
|
-
hasPermission: boolean;
|
|
843
|
-
shouldNotify: boolean;
|
|
844
|
-
}
|
|
845
|
-
```
|
|
846
|
-
|
|
847
|
-
## Zod Integration
|
|
848
|
-
|
|
849
|
-
```typescript
|
|
850
|
-
// Define schema, infer type
|
|
851
|
-
const userSchema = z.object({
|
|
852
|
-
id: z.string(),
|
|
853
|
-
name: z.string(),
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
export type User = z.infer<typeof userSchema>;
|
|
857
|
-
|
|
858
|
-
// Validate API responses
|
|
859
|
-
const response = await get({
|
|
860
|
-
url: "/api/users",
|
|
861
|
-
responseSchema: userSchema,
|
|
862
|
-
});
|
|
863
|
-
```
|
|
864
|
-
|
|
865
|
-
## Type Guards
|
|
866
|
-
|
|
867
|
-
```typescript
|
|
868
|
-
export function isUser(value: unknown): value is User {
|
|
869
|
-
return userSchema.safeParse(value).success;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
// Usage
|
|
873
|
-
if (isUser(data)) {
|
|
874
|
-
console.log(data.name); // TypeScript knows data is User
|
|
875
|
-
}
|
|
876
|
-
```
|
|
877
|
-
|
|
878
|
-
## Constants
|
|
879
|
-
|
|
880
|
-
```typescript
|
|
881
|
-
// Use const assertions
|
|
882
|
-
export const STATUSES = {
|
|
883
|
-
DRAFT: "draft",
|
|
884
|
-
PUBLISHED: "published",
|
|
885
|
-
} as const;
|
|
886
|
-
|
|
887
|
-
export type Status = (typeof STATUSES)[keyof typeof STATUSES];
|
|
888
|
-
```
|
|
889
|
-
|
|
890
|
-
## Utility Types
|
|
891
|
-
|
|
892
|
-
```typescript
|
|
893
|
-
// Built-in utilities
|
|
894
|
-
type PartialUser = Partial<User>;
|
|
895
|
-
type RequiredUser = Required<User>;
|
|
896
|
-
type ReadonlyUser = Readonly<User>;
|
|
897
|
-
type UserIdOnly = Pick<User, "id" | "name">;
|
|
898
|
-
type UserWithoutPassword = Omit<User, "password">;
|
|
899
|
-
type UserMap = Record<string, User>;
|
|
900
|
-
type DefinedString = NonNullable<string | null>;
|
|
901
|
-
|
|
902
|
-
// Function utilities
|
|
903
|
-
type GetUserResult = ReturnType<typeof getUser>;
|
|
904
|
-
type UpdateUserParams = Parameters<typeof updateUser>;
|
|
905
|
-
```
|
|
906
|
-
|
|
907
|
-
## Avoiding `any`
|
|
908
|
-
|
|
909
|
-
```typescript
|
|
910
|
-
// ❌ Bad - Loses type safety
|
|
911
|
-
function process(data: any) { ... }
|
|
912
|
-
|
|
913
|
-
// ✅ Use unknown for truly unknown types
|
|
914
|
-
function process(data: unknown) {
|
|
915
|
-
if (typeof data === "object" && data !== null) {
|
|
916
|
-
// Type guard
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
// ✅ Better - Use generics
|
|
921
|
-
function process<T extends { value: string }>(data: T) {
|
|
922
|
-
return data.value;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// ✅ Best - Use Zod
|
|
926
|
-
const dataSchema = z.object({ value: z.string() });
|
|
927
|
-
function process(data: unknown) {
|
|
928
|
-
const parsed = dataSchema.parse(data);
|
|
929
|
-
return parsed.value;
|
|
930
|
-
}
|
|
931
|
-
```
|
|
932
|
-
|
|
933
|
-
## Generics
|
|
934
|
-
|
|
935
|
-
```typescript
|
|
936
|
-
// Generic function
|
|
937
|
-
function getFirst<T>(array: T[]): T | undefined {
|
|
938
|
-
return array[0];
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
// Generic component
|
|
942
|
-
interface ListProps<T> {
|
|
943
|
-
items: T[];
|
|
944
|
-
renderItem: (item: T) => ReactNode;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
export function List<T>({ items, renderItem }: ListProps<T>) {
|
|
948
|
-
return <>{items.map((item) => renderItem(item))}</>;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
// Constrained generics
|
|
952
|
-
function findById<T extends { id: string }>(items: T[], id: string): T | undefined {
|
|
953
|
-
return items.find((item) => item.id === id);
|
|
954
|
-
}
|
|
955
|
-
```
|
|
956
|
-
|
|
957
|
-
## Discriminated Unions
|
|
958
|
-
|
|
959
|
-
```typescript
|
|
960
|
-
// State machine
|
|
961
|
-
type QueryState<T> =
|
|
962
|
-
| { status: "idle" }
|
|
963
|
-
| { status: "loading" }
|
|
964
|
-
| { status: "success"; data: T }
|
|
965
|
-
| { status: "error"; error: Error };
|
|
966
|
-
|
|
967
|
-
// Automatic type narrowing
|
|
968
|
-
if (state.status === "success") {
|
|
969
|
-
console.log(state.data); // TypeScript knows data exists
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
// Actions
|
|
973
|
-
type Action = { type: "SET_USER"; payload: User } | { type: "CLEAR_USER" };
|
|
974
|
-
|
|
975
|
-
function reducer(state: State, action: Action) {
|
|
976
|
-
switch (action.type) {
|
|
977
|
-
case "SET_USER":
|
|
978
|
-
return { ...state, user: action.payload };
|
|
979
|
-
case "CLEAR_USER":
|
|
980
|
-
return { ...state, user: null };
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
```
|
|
984
|
-
|
|
985
|
-
## Type Narrowing
|
|
986
|
-
|
|
987
|
-
```typescript
|
|
988
|
-
// typeof
|
|
989
|
-
function format(value: string | number) {
|
|
990
|
-
if (typeof value === "string") {
|
|
991
|
-
return value.toUpperCase();
|
|
992
|
-
}
|
|
993
|
-
return value.toFixed(2);
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
// in operator
|
|
997
|
-
function move(animal: Fish | Bird) {
|
|
998
|
-
if ("swim" in animal) {
|
|
999
|
-
animal.swim();
|
|
1000
|
-
} else {
|
|
1001
|
-
animal.fly();
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
// instanceof
|
|
1006
|
-
function handleError(error: unknown) {
|
|
1007
|
-
if (error instanceof Error) {
|
|
1008
|
-
console.log(error.message);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
```
|
|
1012
|
-
|
|
1013
|
-
## Common Patterns
|
|
1014
|
-
|
|
1015
|
-
```typescript
|
|
1016
|
-
// Optional chaining
|
|
1017
|
-
const userName = user?.profile?.name;
|
|
1018
|
-
|
|
1019
|
-
// Nullish coalescing
|
|
1020
|
-
const displayName = userName ?? "Anonymous";
|
|
1021
|
-
|
|
1022
|
-
// Non-null assertion (use sparingly)
|
|
1023
|
-
const user = getUser()!;
|
|
1024
|
-
|
|
1025
|
-
// Template literal types
|
|
1026
|
-
type Route = `/users/${string}` | `/posts/${string}`;
|
|
1027
|
-
type EventHandler = `on${Capitalize<string>}`;
|
|
1028
|
-
```
|
|
1029
|
-
|
|
1030
|
-
## Best Practices
|
|
1031
|
-
|
|
1032
|
-
- **Never use `any`** - Use `unknown` or generics
|
|
1033
|
-
- **Discriminated unions** for state machines
|
|
1034
|
-
- **Zod** for runtime validation
|
|
1035
|
-
- **Type guards** over type assertions
|
|
1036
|
-
- **Const assertions** for readonly values
|
|
1037
|
-
- **Utility types** to transform existing types
|
|
1
|
+
@AGENTS.md
|