@reasonabletech/eslint-config 0.1.0 → 0.2.1
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/CHANGELOG.md +28 -1
- package/docs/concepts/ai-code-safety.md +268 -0
- package/docs/concepts/architecture.md +394 -0
- package/docs/guides/migration.md +47 -0
- package/docs/guides/usage-guide.md +236 -0
- package/docs/index.md +29 -0
- package/docs/reference/api-reference.md +230 -0
- package/docs/reference/base-config.md +118 -0
- package/docs/reference/frameworks/README.md +61 -0
- package/docs/reference/frameworks/next-config.md +187 -0
- package/docs/reference/frameworks/react-config.md +273 -0
- package/docs/tutorials/refactoring-react-for-type-safety.md +411 -0
- package/package.json +3 -2
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# Refactoring React Code for Type Safety
|
|
2
|
+
|
|
3
|
+
A comprehensive guide to updating React components to work with strict TypeScript ESLint rules, focusing on the `@typescript-eslint/strict-boolean-expressions` rule.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This tutorial walks you through refactoring common React patterns that violate strict boolean expression rules. These patterns can cause subtle rendering bugs and inconsistent behavior.
|
|
8
|
+
|
|
9
|
+
## The Problem
|
|
10
|
+
|
|
11
|
+
With `@typescript-eslint/strict-boolean-expressions` enabled, React code that uses "truthiness" checks will produce lint errors. These patterns are problematic because they can render unexpected values like `0`, empty strings, or `[object Object]` instead of nothing.
|
|
12
|
+
|
|
13
|
+
## Common Problematic Patterns
|
|
14
|
+
|
|
15
|
+
### 1. Object Truthiness Checks
|
|
16
|
+
|
|
17
|
+
**❌ Problematic Pattern:**
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
{
|
|
21
|
+
result.symbolIndex && <SymbolSummary result={result} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
streamingMessage && <StreamingMessage message={streamingMessage} />;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**✅ Fixed Pattern:**
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
// Explicit null/undefined checks
|
|
33
|
+
{
|
|
34
|
+
result.symbolIndex != null && <SymbolSummary result={result} />;
|
|
35
|
+
}
|
|
36
|
+
{
|
|
37
|
+
streamingMessage != null && <StreamingMessage message={streamingMessage} />;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Error Object Rendering
|
|
42
|
+
|
|
43
|
+
**❌ Problematic Pattern:**
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
{
|
|
47
|
+
error && <ErrorMessage>{error}</ErrorMessage>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
status.error && <Text color="red"> - {status.error}</Text>;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**✅ Fixed Pattern:**
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// Explicit error checks
|
|
59
|
+
{
|
|
60
|
+
error != null && <ErrorMessage>{error}</ErrorMessage>;
|
|
61
|
+
}
|
|
62
|
+
{
|
|
63
|
+
status.error != null && <Text color="red"> - {status.error}</Text>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Even better - handle error types properly
|
|
67
|
+
{
|
|
68
|
+
error instanceof Error && <ErrorMessage>{error.message}</ErrorMessage>;
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 3. Boolean State Checks
|
|
73
|
+
|
|
74
|
+
**❌ Problematic Pattern:**
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
{
|
|
78
|
+
isDevelopmentBypass && <DevelopmentBanner />;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
isRetrying && <Spinner />;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**✅ Fixed Pattern:**
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
// These are actually already correct! Boolean checks are allowed
|
|
90
|
+
{
|
|
91
|
+
isDevelopmentBypass && <DevelopmentBanner />;
|
|
92
|
+
}
|
|
93
|
+
{
|
|
94
|
+
isRetrying && <Spinner />;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// But for clarity, you can be explicit
|
|
98
|
+
{
|
|
99
|
+
isDevelopmentBypass === true && <DevelopmentBanner />;
|
|
100
|
+
}
|
|
101
|
+
{
|
|
102
|
+
isRetrying === true && <Spinner />;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 4. String and Number Checks
|
|
107
|
+
|
|
108
|
+
**❌ Problematic Pattern:**
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// Common patterns that would fail
|
|
112
|
+
{
|
|
113
|
+
count && <Counter value={count} />;
|
|
114
|
+
} // Could render 0
|
|
115
|
+
{
|
|
116
|
+
user?.name && <UserName name={user.name} />;
|
|
117
|
+
} // Could render empty string
|
|
118
|
+
{
|
|
119
|
+
items && <ItemList items={items} />;
|
|
120
|
+
} // Could render empty array
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**✅ Fixed Pattern:**
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
// Numbers - explicit comparison
|
|
127
|
+
{
|
|
128
|
+
count > 0 && <Counter value={count} />;
|
|
129
|
+
}
|
|
130
|
+
{
|
|
131
|
+
(count ?? 0) > 0 && <Counter value={count} />;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Strings - explicit checks
|
|
135
|
+
{
|
|
136
|
+
user?.name != null && user.name.length > 0 && <UserName name={user.name} />;
|
|
137
|
+
}
|
|
138
|
+
{
|
|
139
|
+
Boolean(user?.name) && <UserName name={user.name} />;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Arrays - check length
|
|
143
|
+
{
|
|
144
|
+
items.length > 0 && <ItemList items={items} />;
|
|
145
|
+
}
|
|
146
|
+
{
|
|
147
|
+
Array.isArray(items) && items.length > 0 && <ItemList items={items} />;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Step-by-Step Refactoring Process
|
|
152
|
+
|
|
153
|
+
### Step 1: Identify Problematic Patterns
|
|
154
|
+
|
|
155
|
+
Run ESLint to find violations:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# In your project directory
|
|
159
|
+
pnpm lint
|
|
160
|
+
|
|
161
|
+
# Look for errors like:
|
|
162
|
+
# "Unexpected value in conditional. A boolean expression is required."
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Step 2: Categorize the Issues
|
|
166
|
+
|
|
167
|
+
Group violations by type:
|
|
168
|
+
|
|
169
|
+
- **Object checks**: `someObject && <Component />`
|
|
170
|
+
- **Array checks**: `someArray && <Component />`
|
|
171
|
+
- **Number checks**: `count && <Component />`
|
|
172
|
+
- **String checks**: `text && <Component />`
|
|
173
|
+
- **Error objects**: `error && <ErrorDisplay />`
|
|
174
|
+
|
|
175
|
+
### Step 3: Apply Appropriate Fixes
|
|
176
|
+
|
|
177
|
+
#### For Object/Value Existence Checks:
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
// Before
|
|
181
|
+
{
|
|
182
|
+
user && <UserProfile user={user} />;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// After
|
|
186
|
+
{
|
|
187
|
+
user != null && <UserProfile user={user} />;
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### For Array Checks:
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
// Before
|
|
195
|
+
{
|
|
196
|
+
items && <ItemList items={items} />;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// After
|
|
200
|
+
{
|
|
201
|
+
items.length > 0 && <ItemList items={items} />;
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### For Numeric Checks:
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
// Before
|
|
209
|
+
{
|
|
210
|
+
count && <Counter count={count} />;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// After - depends on intent
|
|
214
|
+
{
|
|
215
|
+
count > 0 && <Counter count={count} />;
|
|
216
|
+
} // Only positive numbers
|
|
217
|
+
{
|
|
218
|
+
count != null && <Counter count={count} />;
|
|
219
|
+
} // Any number including 0
|
|
220
|
+
{
|
|
221
|
+
count !== 0 && <Counter count={count} />;
|
|
222
|
+
} // Any number except 0
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### For String Checks:
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
// Before
|
|
229
|
+
{
|
|
230
|
+
message && <Alert>{message}</Alert>;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// After - depends on intent
|
|
234
|
+
{
|
|
235
|
+
message != null && message.length > 0 && <Alert>{message}</Alert>;
|
|
236
|
+
}
|
|
237
|
+
{
|
|
238
|
+
Boolean(message) && <Alert>{message}</Alert>;
|
|
239
|
+
}
|
|
240
|
+
{
|
|
241
|
+
message != null && <Alert>{message}</Alert>;
|
|
242
|
+
} // Allow empty strings
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Step 4: Handle Complex Conditions
|
|
246
|
+
|
|
247
|
+
For complex conditions, break them down or use helper functions:
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
// Complex condition
|
|
251
|
+
{
|
|
252
|
+
user && user.permissions && user.permissions.length > 0 && user.isActive && (
|
|
253
|
+
<AdminPanel user={user} />
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Better approach - use helper
|
|
258
|
+
const canShowAdminPanel = (user: User | null): boolean => {
|
|
259
|
+
return (
|
|
260
|
+
user != null &&
|
|
261
|
+
user.permissions != null &&
|
|
262
|
+
user.permissions.length > 0 &&
|
|
263
|
+
user.isActive === true
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// In component
|
|
268
|
+
{
|
|
269
|
+
canShowAdminPanel(user) && <AdminPanel user={user} />;
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Advanced Refactoring Techniques
|
|
274
|
+
|
|
275
|
+
### Custom Hooks for State Logic
|
|
276
|
+
|
|
277
|
+
Instead of complex conditions in JSX, use custom hooks:
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
function useComponentVisibility(
|
|
281
|
+
items: unknown[],
|
|
282
|
+
error: Error | null,
|
|
283
|
+
loading: boolean,
|
|
284
|
+
) {
|
|
285
|
+
return {
|
|
286
|
+
shouldShowItems: items.length > 0 && !loading,
|
|
287
|
+
shouldShowError: error != null,
|
|
288
|
+
shouldShowLoading: loading === true,
|
|
289
|
+
shouldShowEmpty: items.length === 0 && !loading && error == null,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// In component
|
|
294
|
+
const { shouldShowItems, shouldShowError, shouldShowLoading, shouldShowEmpty } =
|
|
295
|
+
useComponentVisibility(items, error, loading);
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<>
|
|
299
|
+
{shouldShowItems && <ItemList items={items} />}
|
|
300
|
+
{shouldShowError && <ErrorDisplay error={error} />}
|
|
301
|
+
{shouldShowLoading && <LoadingSpinner />}
|
|
302
|
+
{shouldShowEmpty && <EmptyState />}
|
|
303
|
+
</>
|
|
304
|
+
);
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Guard Components
|
|
308
|
+
|
|
309
|
+
For reusable conditional logic:
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
interface ConditionalProps {
|
|
313
|
+
condition: boolean;
|
|
314
|
+
children: React.ReactNode;
|
|
315
|
+
fallback?: React.ReactNode;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function Conditional({
|
|
319
|
+
condition,
|
|
320
|
+
children,
|
|
321
|
+
fallback = null,
|
|
322
|
+
}: ConditionalProps) {
|
|
323
|
+
return condition ? <>{children}</> : <>{fallback}</>;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Usage
|
|
327
|
+
<Conditional condition={items.length > 0} fallback={<EmptyState />}>
|
|
328
|
+
<ItemList items={items} />
|
|
329
|
+
</Conditional>;
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Type Guards for Better Safety
|
|
333
|
+
|
|
334
|
+
Create type guards for complex object checks:
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
function hasSymbolIndex(
|
|
338
|
+
result: AnalysisResult,
|
|
339
|
+
): result is AnalysisResult & { symbolIndex: SymbolIndex } {
|
|
340
|
+
return result.symbolIndex != null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// In component
|
|
344
|
+
{
|
|
345
|
+
hasSymbolIndex(result) && <SymbolSummary result={result} />;
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Testing Refactored Components
|
|
350
|
+
|
|
351
|
+
After refactoring, ensure your components still work as expected:
|
|
352
|
+
|
|
353
|
+
```tsx
|
|
354
|
+
describe("RefactoredComponent", () => {
|
|
355
|
+
it("renders content when conditions are met", () => {
|
|
356
|
+
render(<MyComponent items={["item1"]} error={null} loading={false} />);
|
|
357
|
+
expect(screen.getByTestId("item-list")).toBeInTheDocument();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("renders empty state when no items", () => {
|
|
361
|
+
render(<MyComponent items={[]} error={null} loading={false} />);
|
|
362
|
+
expect(screen.getByTestId("empty-state")).toBeInTheDocument();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("renders error when present", () => {
|
|
366
|
+
const error = new Error("Test error");
|
|
367
|
+
render(<MyComponent items={[]} error={error} loading={false} />);
|
|
368
|
+
expect(screen.getByTestId("error-display")).toBeInTheDocument();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("does not render anything when count is 0", () => {
|
|
372
|
+
render(<Counter count={0} />);
|
|
373
|
+
expect(screen.queryByTestId("counter-display")).not.toBeInTheDocument();
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Implementation Checklist
|
|
379
|
+
|
|
380
|
+
- [ ] Run `pnpm lint` to identify all strict-boolean-expressions violations
|
|
381
|
+
- [ ] Group violations by pattern type (object, array, number, string, error)
|
|
382
|
+
- [ ] For each violation:
|
|
383
|
+
- [ ] Determine the intended behavior (show on truthy vs. show on non-null vs. show on non-empty)
|
|
384
|
+
- [ ] Apply appropriate explicit boolean check
|
|
385
|
+
- [ ] Test the component behavior with edge cases (0, "", null, undefined, empty arrays)
|
|
386
|
+
- [ ] Consider extracting complex conditions to helper functions or custom hooks
|
|
387
|
+
- [ ] Update tests to verify correct behavior with edge cases
|
|
388
|
+
- [ ] Run tests to ensure no regressions
|
|
389
|
+
- [ ] Run `pnpm lint` again to verify all violations are fixed
|
|
390
|
+
|
|
391
|
+
## Benefits of This Approach
|
|
392
|
+
|
|
393
|
+
1. **Prevents Rendering Bugs**: No more accidentally rendering `0`, `""`, or `[object Object]`
|
|
394
|
+
2. **Explicit Intent**: Code clearly shows what conditions trigger rendering
|
|
395
|
+
3. **Consistent Architecture**: Same strict patterns across TypeScript and React code
|
|
396
|
+
4. **Better Maintainability**: Conditions are unambiguous and easy to understand
|
|
397
|
+
5. **Type Safety**: Explicit checks work better with TypeScript's type system
|
|
398
|
+
6. **AI Code Safety**: Strict checks prevent subtle bugs in generated code
|
|
399
|
+
|
|
400
|
+
## Additional Resources
|
|
401
|
+
|
|
402
|
+
- [TypeScript ESLint strict-boolean-expressions rule](https://typescript-eslint.io/rules/strict-boolean-expressions/)
|
|
403
|
+
- [React Conditional Rendering Documentation](https://react.dev/learn/conditional-rendering)
|
|
404
|
+
- [Error Handling Standards](../../../../../docs/standards/error-handling.md)
|
|
405
|
+
- [TypeScript Standards](../../../../../docs/standards/typescript-standards.md)
|
|
406
|
+
|
|
407
|
+
## Related Documentation
|
|
408
|
+
|
|
409
|
+
- [React Configuration](../reference/frameworks/react-config.md) — React ESLint configuration details
|
|
410
|
+
- [AI Code Safety](../concepts/ai-code-safety.md) — Why strict linting matters for AI-generated code
|
|
411
|
+
- [Usage Guide](../guides/usage-guide.md) — Setup instructions and troubleshooting
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reasonabletech/eslint-config",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Shared ESLint configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"reasonabletech",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"tsup": "8.5.1",
|
|
80
80
|
"typescript": "5.9.3",
|
|
81
81
|
"vitest": "4.0.18",
|
|
82
|
-
"@reasonabletech/config-typescript": "0.1.
|
|
82
|
+
"@reasonabletech/config-typescript": "0.1.2"
|
|
83
83
|
},
|
|
84
84
|
"license": "MIT",
|
|
85
85
|
"repository": {
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
"dist",
|
|
96
96
|
"!dist/**/*.map",
|
|
97
97
|
"!dist/**/*.tsbuildinfo",
|
|
98
|
+
"docs/**/*",
|
|
98
99
|
"README.md",
|
|
99
100
|
"CHANGELOG.md"
|
|
100
101
|
],
|