claude-toolkit 0.16.0 → 0.17.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/CHANGELOG.md +20 -0
- package/README.md +54 -13
- package/bin/cli.ts +75 -19
- package/package.json +6 -7
- package/src/generator.ts +64 -26
- package/src/refresh-hint.ts +95 -0
- package/bin/postinstall.mjs +0 -69
- package/docs/README.md +0 -59
- package/docs/agents/code-reviewer.md +0 -76
- package/docs/agents/github-workflow.md +0 -98
- package/docs/best-practices/capacitor/README.md +0 -64
- package/docs/best-practices/capacitor/capacitor-8.md +0 -60
- package/docs/best-practices/capacitor/capgo-setup.md +0 -151
- package/docs/best-practices/capacitor/channels-and-rollouts.md +0 -101
- package/docs/best-practices/capacitor/live-updates-ota.md +0 -75
- package/docs/best-practices/capacitor/security-encryption.md +0 -68
- package/docs/best-practices/solidjs/README.md +0 -43
- package/docs/best-practices/solidjs/anti-patterns.md +0 -166
- package/docs/best-practices/solidjs/component-patterns.md +0 -131
- package/docs/best-practices/solidjs/context-and-global-state.md +0 -131
- package/docs/best-practices/solidjs/control-flow.md +0 -124
- package/docs/best-practices/solidjs/data-fetching.md +0 -205
- package/docs/best-practices/solidjs/effects-and-lifecycle.md +0 -113
- package/docs/best-practices/solidjs/performance.md +0 -100
- package/docs/best-practices/solidjs/props-patterns.md +0 -100
- package/docs/best-practices/solidjs/reactivity-model.md +0 -104
- package/docs/best-practices/solidjs/signals-and-state.md +0 -78
- package/docs/best-practices/solidjs/stores-and-nested-state.md +0 -111
- package/docs/best-practices/solidjs/typescript-integration.md +0 -186
- package/docs/best-practices/testing/README.md +0 -84
- package/docs/best-practices/testing/playwright-e2e.md +0 -649
- package/docs/best-practices/testing/storybook-interaction.md +0 -455
- package/docs/best-practices/testing/vitest-unit.md +0 -451
- package/docs/best-practices/typescript/README.md +0 -45
- package/docs/best-practices/typescript/any-and-unknown.md +0 -73
- package/docs/best-practices/typescript/deriving-vs-decoupling.md +0 -83
- package/docs/best-practices/typescript/discriminated-unions.md +0 -75
- package/docs/best-practices/typescript/enums-alternatives.md +0 -72
- package/docs/best-practices/typescript/essential-patterns.md +0 -119
- package/docs/best-practices/typescript/generics-patterns.md +0 -105
- package/docs/best-practices/typescript/micro-opinions.md +0 -87
- package/docs/best-practices/typescript/runtime-validation.md +0 -62
- package/docs/best-practices/typescript/satisfies-operator.md +0 -100
- package/docs/best-practices/typescript/tsconfig-cheat-sheet.md +0 -160
- package/docs/best-practices/typescript/type-organization.md +0 -64
- package/docs/best-practices/typescript/type-vs-interface.md +0 -80
- package/docs/commands/code-quality.md +0 -36
- package/docs/commands/onboard.md +0 -63
- package/docs/commands/pr-review.md +0 -91
- package/docs/commands/pr-summary.md +0 -50
- package/docs/commands/proto-check.md +0 -59
- package/docs/commands/ticket.md +0 -56
- package/docs/skills/code-style.md +0 -26
- package/docs/skills/systematic-debugging.md +0 -70
- package/docs/skills/testing-patterns.md +0 -140
- package/docs/skills/typescript-conventions.md +0 -283
- package/docs/skills/verification-before-completion.md +0 -103
- package/docs/stacks/capacitor-ota.md +0 -145
- package/docs/stacks/cloudflare-d1-kv.md +0 -144
- package/docs/stacks/esnext-idioms.md +0 -25
- package/docs/stacks/i18n-typesafe.md +0 -141
- package/docs/stacks/playwright-patterns.md +0 -179
- package/docs/stacks/protobuf-contracts.md +0 -85
- package/docs/stacks/rust-wasm-patterns.md +0 -106
- package/docs/stacks/solidjs-patterns.md +0 -110
- package/docs/stacks/storybook-patterns.md +0 -177
- package/docs/stacks/vanilla-extract-patterns.md +0 -115
- package/docs/stacks/vite-vitest-patterns.md +0 -485
|
@@ -1,451 +0,0 @@
|
|
|
1
|
-
# Vitest Unit Testing
|
|
2
|
-
|
|
3
|
-
> Sources: [Vitest 4.0 Announcement](https://voidzero.dev/posts/announcing-vitest-4), [Vitest 4.1 Blog](https://main.vitest.dev/blog/vitest-4-1), [Vitest Docs](https://vitest.dev/), [SolidJS Testing Docs](https://docs.solidjs.com/guides/testing), [@solidjs/testing-library](https://github.com/solidjs/solid-testing-library)
|
|
4
|
-
|
|
5
|
-
Unit tests form the base of the testing pyramid. They are fast, isolated, and cover pure logic, reactive primitives, and component behavior in a JSDOM environment.
|
|
6
|
-
|
|
7
|
-
## AAA Pattern
|
|
8
|
-
|
|
9
|
-
Every test follows **Arrange-Act-Assert**. Keep each phase visually distinct:
|
|
10
|
-
|
|
11
|
-
```typescript
|
|
12
|
-
it("should format currency correctly", () => {
|
|
13
|
-
// Arrange
|
|
14
|
-
const amount = 1234.5;
|
|
15
|
-
|
|
16
|
-
// Act
|
|
17
|
-
const result = formatCurrency(amount, "NZD");
|
|
18
|
-
|
|
19
|
-
// Assert
|
|
20
|
-
expect(result).toBe("$1,234.50");
|
|
21
|
-
});
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
For simple tests, the phases can be implicit. For complex tests, add blank lines between them.
|
|
25
|
-
|
|
26
|
-
## Naming Conventions
|
|
27
|
-
|
|
28
|
-
- **Files**: `*.test.ts` / `*.test.tsx`, co-located with source or in `tests/`
|
|
29
|
-
- **Describe blocks**: Name the unit under test (`describe("formatCurrency", ...)`)
|
|
30
|
-
- **It blocks**: Describe behavior, not implementation ("should return loading state when resource is pending", not "should set isLoading to true")
|
|
31
|
-
|
|
32
|
-
## Testing SolidJS Components
|
|
33
|
-
|
|
34
|
-
### render() Takes a Callback
|
|
35
|
-
|
|
36
|
-
This is different from React Testing Library. SolidJS needs the callback form to establish a reactive root:
|
|
37
|
-
|
|
38
|
-
```tsx
|
|
39
|
-
import { render, screen } from "@solidjs/testing-library";
|
|
40
|
-
import { Button } from "../src/components/Button";
|
|
41
|
-
|
|
42
|
-
it("should render button text", () => {
|
|
43
|
-
render(() => <Button label="Click me" />);
|
|
44
|
-
expect(screen.getByRole("button")).toHaveTextContent("Click me");
|
|
45
|
-
});
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### Testing Signals with renderHook
|
|
49
|
-
|
|
50
|
-
For hooks/composables that return reactive state:
|
|
51
|
-
|
|
52
|
-
```typescript
|
|
53
|
-
import { renderHook } from "@solidjs/testing-library";
|
|
54
|
-
import { useCounter } from "../src/hooks/useCounter";
|
|
55
|
-
|
|
56
|
-
it("should increment counter", () => {
|
|
57
|
-
const { result } = renderHook(useCounter);
|
|
58
|
-
expect(result.count).toBe(0);
|
|
59
|
-
result.increment();
|
|
60
|
-
expect(result.count).toBe(1);
|
|
61
|
-
});
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### Testing Signals with createRoot
|
|
65
|
-
|
|
66
|
-
For primitives that don't need component context, `createRoot` is sufficient and faster:
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
import { createRoot, createSignal, createMemo } from "solid-js";
|
|
70
|
-
|
|
71
|
-
it("should compute derived value", () => {
|
|
72
|
-
createRoot((dispose) => {
|
|
73
|
-
const [count, setCount] = createSignal(0);
|
|
74
|
-
const doubled = createMemo(() => count() * 2);
|
|
75
|
-
|
|
76
|
-
expect(doubled()).toBe(0);
|
|
77
|
-
setCount(5);
|
|
78
|
-
expect(doubled()).toBe(10);
|
|
79
|
-
|
|
80
|
-
dispose();
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### Testing Effects with testEffect
|
|
86
|
-
|
|
87
|
-
Effects are async in SolidJS. The `testEffect` utility provides a `done` callback for async assertion:
|
|
88
|
-
|
|
89
|
-
```typescript
|
|
90
|
-
import { testEffect } from "@solidjs/testing-library";
|
|
91
|
-
import { createSignal, createEffect } from "solid-js";
|
|
92
|
-
|
|
93
|
-
it("should react to signal changes", () => {
|
|
94
|
-
const [value, setValue] = createSignal(0);
|
|
95
|
-
|
|
96
|
-
return testEffect((done) =>
|
|
97
|
-
createEffect((run: number = 0) => {
|
|
98
|
-
if (run === 0) {
|
|
99
|
-
expect(value()).toBe(0);
|
|
100
|
-
setValue(1);
|
|
101
|
-
} else if (run === 1) {
|
|
102
|
-
expect(value()).toBe(1);
|
|
103
|
-
done();
|
|
104
|
-
}
|
|
105
|
-
return run + 1;
|
|
106
|
-
})
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### Testing Resources (createResource)
|
|
112
|
-
|
|
113
|
-
Resources trigger Suspense. Wrap in `<Suspense>` and assert loading/data/error states:
|
|
114
|
-
|
|
115
|
-
```tsx
|
|
116
|
-
import { render, screen } from "@solidjs/testing-library";
|
|
117
|
-
import { Suspense } from "solid-js";
|
|
118
|
-
|
|
119
|
-
it("should show loading then data", async () => {
|
|
120
|
-
render(() => (
|
|
121
|
-
<Suspense fallback={<div>Loading...</div>}>
|
|
122
|
-
<MyResourceComponent />
|
|
123
|
-
</Suspense>
|
|
124
|
-
));
|
|
125
|
-
|
|
126
|
-
expect(screen.getByText("Loading...")).toBeInTheDocument();
|
|
127
|
-
expect(await screen.findByText("Data loaded")).toBeInTheDocument();
|
|
128
|
-
});
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## Mocking Strategies
|
|
132
|
-
|
|
133
|
-
### Decision Framework
|
|
134
|
-
|
|
135
|
-
| Scenario | Tool |
|
|
136
|
-
|---|---|
|
|
137
|
-
| Replace entire module | `vi.mock("module")` |
|
|
138
|
-
| Spy on a specific method | `vi.spyOn(obj, "method")` |
|
|
139
|
-
| Network requests | MSW (`msw/node`) |
|
|
140
|
-
| Vary mock per test | `vi.hoisted` + `mockReturnValue` |
|
|
141
|
-
| Non-hoisted dynamic mock | `vi.doMock` |
|
|
142
|
-
|
|
143
|
-
### vi.mock with vi.hoisted (Recommended Pattern)
|
|
144
|
-
|
|
145
|
-
`vi.hoisted` lets you declare mock references that are accessible inside `vi.mock` (which is hoisted to the top of the file):
|
|
146
|
-
|
|
147
|
-
```typescript
|
|
148
|
-
const { mockFetch } = vi.hoisted(() => ({
|
|
149
|
-
mockFetch: vi.fn(),
|
|
150
|
-
}));
|
|
151
|
-
|
|
152
|
-
vi.mock("../src/api/client", () => ({
|
|
153
|
-
fetchData: mockFetch,
|
|
154
|
-
}));
|
|
155
|
-
|
|
156
|
-
it("should handle API error", () => {
|
|
157
|
-
mockFetch.mockRejectedValue(new Error("Network error"));
|
|
158
|
-
// ... test
|
|
159
|
-
});
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### vi.spyOn for Partial Mocking
|
|
163
|
-
|
|
164
|
-
When you only need to intercept one method while keeping the rest real:
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
import * as utils from "../src/utils";
|
|
168
|
-
|
|
169
|
-
it("should call logger", () => {
|
|
170
|
-
const spy = vi.spyOn(utils, "log");
|
|
171
|
-
doSomething();
|
|
172
|
-
expect(spy).toHaveBeenCalledWith("action completed");
|
|
173
|
-
spy.mockRestore();
|
|
174
|
-
});
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### MSW for Network Mocking
|
|
178
|
-
|
|
179
|
-
Mock Service Worker intercepts at the network level, giving realistic integration tests:
|
|
180
|
-
|
|
181
|
-
```typescript
|
|
182
|
-
import { setupServer } from "msw/node";
|
|
183
|
-
import { http, HttpResponse } from "msw";
|
|
184
|
-
|
|
185
|
-
const server = setupServer(
|
|
186
|
-
http.get("/api/user", () => HttpResponse.json({ name: "Alice" }))
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
beforeAll(() => server.listen());
|
|
190
|
-
afterEach(() => server.resetHandlers());
|
|
191
|
-
afterAll(() => server.close());
|
|
192
|
-
|
|
193
|
-
it("should display user name", async () => {
|
|
194
|
-
render(() => <UserProfile />);
|
|
195
|
-
expect(await screen.findByText("Alice")).toBeInTheDocument();
|
|
196
|
-
});
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
### mockThrow / mockThrowOnce (Vitest 4.1+)
|
|
200
|
-
|
|
201
|
-
Concise error path testing without wrapping in functions:
|
|
202
|
-
|
|
203
|
-
```typescript
|
|
204
|
-
mockFn.mockThrow(new Error("failed"));
|
|
205
|
-
mockFn.mockThrowOnce(new Error("first call fails"));
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
### Type-Safe Mocks
|
|
209
|
-
|
|
210
|
-
Use `vi.mocked()` for typed access to mocked functions:
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
import { fetchUser } from "../src/api";
|
|
214
|
-
vi.mock("../src/api");
|
|
215
|
-
|
|
216
|
-
const mockedFetchUser = vi.mocked(fetchUser);
|
|
217
|
-
mockedFetchUser.mockResolvedValue({ name: "Alice" });
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
## Snapshot Testing
|
|
221
|
-
|
|
222
|
-
### When to Use
|
|
223
|
-
|
|
224
|
-
- Serialized objects, API response shapes, configuration objects
|
|
225
|
-
- Small component output (buttons, cards, form fields)
|
|
226
|
-
|
|
227
|
-
### When NOT to Use
|
|
228
|
-
|
|
229
|
-
- Large pages or complex component trees
|
|
230
|
-
- Frequently changing UI
|
|
231
|
-
- Dynamic content (timestamps, IDs, random values)
|
|
232
|
-
|
|
233
|
-
### Prefer Inline Snapshots
|
|
234
|
-
|
|
235
|
-
Keeps expected output visible in the test file:
|
|
236
|
-
|
|
237
|
-
```typescript
|
|
238
|
-
expect(formatUser(user)).toMatchInlineSnapshot(`
|
|
239
|
-
{
|
|
240
|
-
"name": "Alice",
|
|
241
|
-
"role": "admin",
|
|
242
|
-
}
|
|
243
|
-
`);
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
### Handle Dynamic Values
|
|
247
|
-
|
|
248
|
-
```typescript
|
|
249
|
-
expect(result).toMatchObject({
|
|
250
|
-
id: expect.any(String),
|
|
251
|
-
createdAt: expect.any(Date),
|
|
252
|
-
name: "Alice",
|
|
253
|
-
});
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
### Snapshot Hygiene
|
|
257
|
-
|
|
258
|
-
- Review snapshot diffs as carefully as code diffs in PRs
|
|
259
|
-
- Run `vitest --update` only intentionally, never blindly
|
|
260
|
-
- CI should fail on obsolete snapshots
|
|
261
|
-
|
|
262
|
-
## Type Testing
|
|
263
|
-
|
|
264
|
-
Vitest has built-in type testing (since 2.1). Verify your types work correctly alongside runtime:
|
|
265
|
-
|
|
266
|
-
```typescript
|
|
267
|
-
import { expectTypeOf } from "vitest";
|
|
268
|
-
|
|
269
|
-
it("should return correct type", () => {
|
|
270
|
-
const result = parseConfig(rawConfig);
|
|
271
|
-
expectTypeOf(result).toEqualTypeOf<AppConfig>();
|
|
272
|
-
expectTypeOf(result.port).toBeNumber();
|
|
273
|
-
});
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
## Coverage
|
|
277
|
-
|
|
278
|
-
### Use V8 Provider
|
|
279
|
-
|
|
280
|
-
Since Vitest 3.2, V8 coverage uses AST-based remapping that matches Istanbul accuracy with ~10% overhead vs Istanbul's ~300%. Always use V8:
|
|
281
|
-
|
|
282
|
-
```typescript
|
|
283
|
-
// vite.config.ts
|
|
284
|
-
test: {
|
|
285
|
-
coverage: {
|
|
286
|
-
provider: "v8",
|
|
287
|
-
include: ["src/**/*.{ts,tsx}"],
|
|
288
|
-
exclude: [
|
|
289
|
-
"src/**/*.css.ts", // vanilla-extract style files
|
|
290
|
-
"src/**/index.ts", // barrel exports
|
|
291
|
-
"src/**/*.d.ts", // type declarations
|
|
292
|
-
"src/locales/**", // i18n generated files
|
|
293
|
-
],
|
|
294
|
-
reporter: ["text", "html", "lcov"],
|
|
295
|
-
thresholds: {
|
|
296
|
-
statements: 80,
|
|
297
|
-
branches: 70,
|
|
298
|
-
functions: 80,
|
|
299
|
-
lines: 80,
|
|
300
|
-
},
|
|
301
|
-
},
|
|
302
|
-
}
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
### Coverage Recommendations
|
|
306
|
-
|
|
307
|
-
- Start with moderate thresholds (70-80%) and ratchet up as coverage grows
|
|
308
|
-
- Focus on **branch coverage** -- it catches more real bugs than line coverage
|
|
309
|
-
- Use per-glob overrides for critical paths (e.g., `src/utils/**` at 95%)
|
|
310
|
-
- Use `/* v8 ignore start */` / `/* v8 ignore stop */` for intentionally uncovered code
|
|
311
|
-
|
|
312
|
-
## Vitest 4.x Features
|
|
313
|
-
|
|
314
|
-
### Test Tags
|
|
315
|
-
|
|
316
|
-
Define tags with per-tag configuration. Useful for separating fast/slow tests:
|
|
317
|
-
|
|
318
|
-
```typescript
|
|
319
|
-
// vite.config.ts
|
|
320
|
-
test: {
|
|
321
|
-
tags: {
|
|
322
|
-
slow: { timeout: 30_000 },
|
|
323
|
-
flaky: { retry: 3 },
|
|
324
|
-
},
|
|
325
|
-
}
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
it.tags(["slow"])("should process large dataset", async () => {
|
|
330
|
-
// ...
|
|
331
|
-
});
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
### Builder-Pattern Fixtures
|
|
335
|
-
|
|
336
|
-
The idiomatic way to share setup in Vitest 4.x. TypeScript infers fixture types automatically:
|
|
337
|
-
|
|
338
|
-
```typescript
|
|
339
|
-
const myTest = test
|
|
340
|
-
.extend({ config: { timeout: 5000 } })
|
|
341
|
-
.extend({
|
|
342
|
-
server: async ({ config }, { onCleanup }) => {
|
|
343
|
-
const srv = await startServer(config);
|
|
344
|
-
onCleanup(() => srv.close());
|
|
345
|
-
return srv;
|
|
346
|
-
},
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
myTest("should connect to server", ({ server }) => {
|
|
350
|
-
expect(server.isRunning()).toBe(true);
|
|
351
|
-
});
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
### Detect Async Leaks
|
|
355
|
-
|
|
356
|
-
Reports leaked async resources with source locations. Enable while debugging:
|
|
357
|
-
|
|
358
|
-
```typescript
|
|
359
|
-
test: {
|
|
360
|
-
detectAsyncLeaks: true, // Disable in CI (adds overhead)
|
|
361
|
-
}
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
## Performance
|
|
365
|
-
|
|
366
|
-
### Pool Configuration
|
|
367
|
-
|
|
368
|
-
| Pool | Best For |
|
|
369
|
-
|---|---|
|
|
370
|
-
| `threads` (default) | General use, SolidJS + JSDOM tests |
|
|
371
|
-
| `forks` | Better isolation, more overhead |
|
|
372
|
-
| `vmThreads` | VM contexts with worker parallelism |
|
|
373
|
-
|
|
374
|
-
### Sharding for CI
|
|
375
|
-
|
|
376
|
-
Split tests across CI machines:
|
|
377
|
-
|
|
378
|
-
```bash
|
|
379
|
-
# Machine 1
|
|
380
|
-
vitest run --shard=1/3 --reporter=blob
|
|
381
|
-
|
|
382
|
-
# Machine 2
|
|
383
|
-
vitest run --shard=2/3 --reporter=blob
|
|
384
|
-
|
|
385
|
-
# Machine 3
|
|
386
|
-
vitest run --shard=3/3 --reporter=blob
|
|
387
|
-
|
|
388
|
-
# Merge
|
|
389
|
-
vitest merge-reports
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
### Other Tips
|
|
393
|
-
|
|
394
|
-
- Use `test.concurrent` for independent async tests
|
|
395
|
-
- Keep test files small and focused (Vitest parallelizes at the file level)
|
|
396
|
-
- Avoid heavy setup in `beforeAll` that could be lazy-loaded
|
|
397
|
-
- Use `--no-isolate` only for pure function test files, never for component tests
|
|
398
|
-
|
|
399
|
-
## Recommended Config
|
|
400
|
-
|
|
401
|
-
```typescript
|
|
402
|
-
// vite.config.ts (test section)
|
|
403
|
-
test: {
|
|
404
|
-
environment: "jsdom",
|
|
405
|
-
globals: true,
|
|
406
|
-
setupFiles: ["./tests/setup.ts"],
|
|
407
|
-
include: ["src/**/*.test.{ts,tsx}", "tests/**/*.test.{ts,tsx}"],
|
|
408
|
-
restoreMocks: true,
|
|
409
|
-
coverage: {
|
|
410
|
-
provider: "v8",
|
|
411
|
-
include: ["src/**/*.{ts,tsx}"],
|
|
412
|
-
exclude: [
|
|
413
|
-
"src/**/*.css.ts",
|
|
414
|
-
"src/**/index.ts",
|
|
415
|
-
"src/**/*.d.ts",
|
|
416
|
-
"src/locales/**",
|
|
417
|
-
],
|
|
418
|
-
reporter: ["text", "html", "lcov"],
|
|
419
|
-
thresholds: {
|
|
420
|
-
statements: 80,
|
|
421
|
-
branches: 70,
|
|
422
|
-
functions: 80,
|
|
423
|
-
lines: 80,
|
|
424
|
-
},
|
|
425
|
-
},
|
|
426
|
-
}
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
```typescript
|
|
430
|
-
// tests/setup.ts
|
|
431
|
-
import "@testing-library/jest-dom/vitest";
|
|
432
|
-
|
|
433
|
-
afterEach(() => {
|
|
434
|
-
vi.clearAllTimers();
|
|
435
|
-
});
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
## Anti-Patterns
|
|
439
|
-
|
|
440
|
-
| Anti-Pattern | Fix |
|
|
441
|
-
|---|---|
|
|
442
|
-
| Testing implementation details (internal state, private methods) | Test behavior and public API only |
|
|
443
|
-
| Large snapshots (100+ lines) | Break into small focused snapshots or use explicit assertions |
|
|
444
|
-
| Blind `--update` on snapshot failures | Review each diff individually |
|
|
445
|
-
| `vi.mock` without cleanup | Enable `restoreMocks: true` in config |
|
|
446
|
-
| Shared mutable state across tests | Reset state in `beforeEach` or use fixtures |
|
|
447
|
-
| Testing framework code (that SolidJS reactivity works) | Test your logic, not the framework |
|
|
448
|
-
| `setTimeout` in tests for async waits | Use `waitFor`, `findBy*`, or `vi.advanceTimersByTime` |
|
|
449
|
-
| Over-mocking (mocking everything) | Mock only external boundaries (network, timers, platform APIs) |
|
|
450
|
-
| Destructuring SolidJS props in tests | Same rule as production -- never destructure props |
|
|
451
|
-
| Using `as any` in test code | If you need it, the API under test is wrong |
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# Matt Pocock's TypeScript Best Practices
|
|
2
|
-
|
|
3
|
-
> A curated collection of TypeScript best practices, opinions, and patterns from [Matt Pocock](https://www.mattpocock.com/) and [Total TypeScript](https://www.totaltypescript.com/).
|
|
4
|
-
|
|
5
|
-
Matt Pocock is one of the most influential TypeScript educators. These documents capture his opinionated, production-tested guidance — not generic advice, but specific stances backed by reasoning.
|
|
6
|
-
|
|
7
|
-
## Core Choices
|
|
8
|
-
|
|
9
|
-
Foundational decisions that shape every TypeScript project.
|
|
10
|
-
|
|
11
|
-
| Guide | Summary |
|
|
12
|
-
|---|---|
|
|
13
|
-
| [Type vs Interface](type-vs-interface.md) | Default to `type`. Use `interface` only for `extends`. |
|
|
14
|
-
| [Enums & Alternatives](enums-alternatives.md) | Avoid enums. Use `as const` objects with derived unions. |
|
|
15
|
-
| [any & unknown](any-and-unknown.md) | Ban `any` by default. Know the two legitimate exceptions. |
|
|
16
|
-
| [TSConfig Cheat Sheet](tsconfig-cheat-sheet.md) | Recommended compiler options for every project shape. |
|
|
17
|
-
|
|
18
|
-
## Patterns
|
|
19
|
-
|
|
20
|
-
Type-level patterns that make code safer and more expressive.
|
|
21
|
-
|
|
22
|
-
| Guide | Summary |
|
|
23
|
-
|---|---|
|
|
24
|
-
| [Discriminated Unions](discriminated-unions.md) | Model states as unions — make illegal states unrepresentable. |
|
|
25
|
-
| [Generics Patterns](generics-patterns.md) | Three patterns: types-to-types, types-to-functions, inference. |
|
|
26
|
-
| [The `satisfies` Operator](satisfies-operator.md) | Five practical uses for type validation without losing inference. |
|
|
27
|
-
| [Essential Patterns](essential-patterns.md) | Branded types, globals, assertion functions, classes. |
|
|
28
|
-
|
|
29
|
-
## Architecture
|
|
30
|
-
|
|
31
|
-
How to organize and connect types across a codebase.
|
|
32
|
-
|
|
33
|
-
| Guide | Summary |
|
|
34
|
-
|---|---|
|
|
35
|
-
| [Type Organization](type-organization.md) | Colocate, share at folder level, or extract to a package. |
|
|
36
|
-
| [Deriving vs Decoupling](deriving-vs-decoupling.md) | Derive when tightly coupled. Decouple across concerns. |
|
|
37
|
-
| [Runtime Validation](runtime-validation.md) | Use Zod at trust boundaries. Skip it for controlled inputs. |
|
|
38
|
-
|
|
39
|
-
## Micro-Opinions
|
|
40
|
-
|
|
41
|
-
Small but specific stances on everyday TypeScript choices.
|
|
42
|
-
|
|
43
|
-
| Guide | Summary |
|
|
44
|
-
|---|---|
|
|
45
|
-
| [Micro-Opinions](micro-opinions.md) | Return types, `const` vs `let`, `T[]` vs `Array<T>`, method shorthand, `Function` type. |
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# `any` & `unknown`
|
|
2
|
-
|
|
3
|
-
> Source: [any Considered Harmful, Except For These Cases](https://www.totaltypescript.com/any-considered-harmful) — Matt Pocock
|
|
4
|
-
|
|
5
|
-
## The Rule
|
|
6
|
-
|
|
7
|
-
**Ban `any` from your codebase.** Enable the ESLint rule that prevents its use and avoid it wherever possible.
|
|
8
|
-
|
|
9
|
-
`any` disables TypeScript's core value proposition: type checking, autocomplete, and safety guarantees. It's a contagion — once `any` enters an expression, it spreads through assignments and return values.
|
|
10
|
-
|
|
11
|
-
## What to Use Instead
|
|
12
|
-
|
|
13
|
-
| Situation | Use |
|
|
14
|
-
|---|---|
|
|
15
|
-
| Type is genuinely unknown | `unknown` (then narrow before use) |
|
|
16
|
-
| Type varies but has a consistent shape | Generics with constraints |
|
|
17
|
-
| You know the data shape | The specific type |
|
|
18
|
-
|
|
19
|
-
```typescript
|
|
20
|
-
// Instead of any
|
|
21
|
-
function parse(input: unknown): User {
|
|
22
|
-
if (typeof input === "object" && input !== null && "name" in input) {
|
|
23
|
-
return input as User;
|
|
24
|
-
}
|
|
25
|
-
throw new Error("Invalid input");
|
|
26
|
-
}
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## The Two Legitimate Exceptions
|
|
30
|
-
|
|
31
|
-
Matt identifies two advanced scenarios where `any` is the right choice.
|
|
32
|
-
|
|
33
|
-
### Exception 1: Type Argument Constraints in Generics
|
|
34
|
-
|
|
35
|
-
When building generic utility types, `unknown` in constraints can be too restrictive:
|
|
36
|
-
|
|
37
|
-
```typescript
|
|
38
|
-
// This is too restrictive — only works with specific function shapes
|
|
39
|
-
type ReturnType<T extends (...args: unknown[]) => unknown> = ...
|
|
40
|
-
|
|
41
|
-
// any[] constraint is correct — "we don't care what the function accepts"
|
|
42
|
-
type ReturnType<T extends (...args: any[]) => any> = ...
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Using `any[]` here is intentional: you're declaring that the constraint should accept **any function**, not that you're bypassing safety.
|
|
46
|
-
|
|
47
|
-
### Exception 2: Conditional Types in Generic Functions
|
|
48
|
-
|
|
49
|
-
TypeScript's type narrowing sometimes can't verify that conditional type logic matches runtime behavior:
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
function processValue<T extends string | number>(
|
|
53
|
-
value: T
|
|
54
|
-
): T extends string ? string[] : number {
|
|
55
|
-
if (typeof value === "string") {
|
|
56
|
-
return value.split("") as any; // TypeScript can't narrow conditional return types
|
|
57
|
-
}
|
|
58
|
-
return (value as number) * 2 as any;
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
In these cases, Matt recommends using `as any` **and adding a unit test** to validate the function's behavior at runtime.
|
|
63
|
-
|
|
64
|
-
## The Pragmatic Approach
|
|
65
|
-
|
|
66
|
-
When you genuinely need `any`, use an `eslint-disable` comment to make it explicit and reviewable:
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
-
type Callback = (...args: any[]) => void;
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
This creates a searchable record of every `any` in your codebase, making them easy to audit and justify in code review.
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# Deriving vs Decoupling
|
|
2
|
-
|
|
3
|
-
> Source: [Deriving vs Decoupling: When NOT To Be A TypeScript Wizard](https://www.totaltypescript.com/deriving-vs-decoupling) — Matt Pocock
|
|
4
|
-
|
|
5
|
-
## The Distinction
|
|
6
|
-
|
|
7
|
-
- **Deriving** = creating a type that depends on another type's structure (`Pick`, `Omit`, `typeof`, mapped types)
|
|
8
|
-
- **Decoupling** = creating independent types with no dependency between them
|
|
9
|
-
|
|
10
|
-
The temptation to derive comes from TypeScript's powerful type manipulation. But cleverness isn't always correctness.
|
|
11
|
-
|
|
12
|
-
## When to Decouple
|
|
13
|
-
|
|
14
|
-
Decouple when types serve **different concerns** — different responsibilities with different reasons to change.
|
|
15
|
-
|
|
16
|
-
```typescript
|
|
17
|
-
// Database layer
|
|
18
|
-
type User = {
|
|
19
|
-
id: string;
|
|
20
|
-
name: string;
|
|
21
|
-
email: string;
|
|
22
|
-
imageUrl: string;
|
|
23
|
-
passwordHash: string;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
// UI layer — DON'T derive from User
|
|
27
|
-
type AvatarProps = {
|
|
28
|
-
imageUrl: string;
|
|
29
|
-
name: string;
|
|
30
|
-
};
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Why not `Pick<User, "imageUrl" | "name">`? Because:
|
|
34
|
-
- `User` lives in database code; `AvatarProps` is UI logic
|
|
35
|
-
- They have different reasons to change
|
|
36
|
-
- If `User.imageUrl` is renamed to `User.avatarUrl`, should every UI component break? Probably not.
|
|
37
|
-
- The component should remain portable even if the data model moves
|
|
38
|
-
|
|
39
|
-
As Matt puts it: "It's smarter to do the simple thing."
|
|
40
|
-
|
|
41
|
-
## When to Derive
|
|
42
|
-
|
|
43
|
-
Derive when types share a **common concern** and are tightly coupled by design.
|
|
44
|
-
|
|
45
|
-
### From `as const` objects
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
const albumTypes = {
|
|
49
|
-
CD: "cd",
|
|
50
|
-
VINYL: "vinyl",
|
|
51
|
-
DIGITAL: "digital",
|
|
52
|
-
} as const;
|
|
53
|
-
|
|
54
|
-
type AlbumType = (typeof albumTypes)[keyof typeof albumTypes];
|
|
55
|
-
// "cd" | "vinyl" | "digital"
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Maintaining a separate union and object would mean updating two places for every change — pure busywork.
|
|
59
|
-
|
|
60
|
-
### Related variants
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
type User = {
|
|
64
|
-
id: string;
|
|
65
|
-
name: string;
|
|
66
|
-
email: string;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
type UserWithoutId = Omit<User, "id">;
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
These are directly related — a `UserWithoutId` is meaningless outside the context of `User`.
|
|
73
|
-
|
|
74
|
-
## Decision Framework
|
|
75
|
-
|
|
76
|
-
> "The decision to derive or decouple is all about reducing your future workload."
|
|
77
|
-
|
|
78
|
-
| Signal | Action |
|
|
79
|
-
|---|---|
|
|
80
|
-
| Changing one type should always change the other | **Derive** |
|
|
81
|
-
| Types serve different layers or concerns | **Decouple** |
|
|
82
|
-
| Derivation adds complexity for marginal DRY benefit | **Decouple** |
|
|
83
|
-
| Types share the same source of truth | **Derive** |
|