memory-journal-mcp 7.0.1 → 7.2.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/README.md +75 -66
- package/dist/{chunk-6J4RPJ4I.js → chunk-GR4T3SRW.js} +146 -105
- package/dist/{chunk-ARLH46WS.js → chunk-IWKLHSPU.js} +89 -3
- package/dist/{chunk-2BJHLTYP.js → chunk-ORV7ZZOE.js} +1086 -86
- package/dist/cli.js +30 -4
- package/dist/github-integration-2TFMXHIJ.js +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -3
- package/dist/{tools-FFFGXIKN.js → tools-CXR2FEB2.js} +2 -2
- package/package.json +2 -2
- package/skills/README.md +77 -0
- package/skills/autonomous-dev/SKILL.md +56 -0
- package/skills/bin/sync.js +50 -0
- package/skills/bun/SKILL.md +156 -0
- package/skills/github-commander/SKILL.md +1 -1
- package/skills/github-commander/workflows/code-quality-audit.md +7 -5
- package/skills/github-commander/workflows/issue-triage.md +13 -4
- package/skills/github-commander/workflows/milestone-sprint.md +9 -1
- package/skills/github-commander/workflows/perf-audit.md +2 -0
- package/skills/github-commander/workflows/pr-review.md +9 -3
- package/skills/github-commander/workflows/roadmap-kickoff.md +79 -0
- package/skills/github-commander/workflows/security-audit.md +3 -3
- package/skills/github-commander/workflows/update-deps.md +2 -2
- package/skills/gitlab/SKILL.md +115 -0
- package/skills/gitlab/package-lock.json +392 -0
- package/skills/gitlab/package.json +14 -0
- package/skills/gitlab/scripts/gitlab-client.ts +125 -0
- package/skills/gitlab/scripts/gitlab-helper.ts +80 -0
- package/skills/golang/SKILL.md +54 -0
- package/skills/mysql/SKILL.md +30 -0
- package/skills/package.json +48 -0
- package/skills/playwright-standard/SKILL.md +58 -0
- package/skills/playwright-standard/examples/fixtures.ts +66 -0
- package/skills/playwright-standard/examples/type-stubs.d.ts +10 -0
- package/skills/playwright-standard/references/advanced-scenarios.md +59 -0
- package/skills/playwright-standard/references/infrastructure.md +43 -0
- package/skills/postgres/SKILL.md +33 -0
- package/skills/react-best-practices/AGENTS.md +2883 -0
- package/skills/react-best-practices/README.md +127 -0
- package/skills/react-best-practices/SKILL.md +138 -0
- package/skills/react-best-practices/metadata.json +17 -0
- package/skills/react-best-practices/rules/_sections.md +46 -0
- package/skills/react-best-practices/rules/_template.md +28 -0
- package/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/skills/react-best-practices/rules/advanced-init-once.md +42 -0
- package/skills/react-best-practices/rules/advanced-use-latest.md +39 -0
- package/skills/react-best-practices/rules/async-api-routes.md +35 -0
- package/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/skills/react-best-practices/rules/async-dependencies.md +48 -0
- package/skills/react-best-practices/rules/async-parallel.md +24 -0
- package/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/skills/react-best-practices/rules/bundle-conditional.md +37 -0
- package/skills/react-best-practices/rules/bundle-defer-third-party.md +48 -0
- package/skills/react-best-practices/rules/bundle-dynamic-imports.md +34 -0
- package/skills/react-best-practices/rules/bundle-preload.md +44 -0
- package/skills/react-best-practices/rules/client-event-listeners.md +78 -0
- package/skills/react-best-practices/rules/client-localstorage-schema.md +74 -0
- package/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/skills/react-best-practices/rules/js-batch-dom-css.md +110 -0
- package/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/skills/react-best-practices/rules/js-cache-storage.md +68 -0
- package/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/skills/react-best-practices/rules/js-length-check-first.md +50 -0
- package/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/skills/react-best-practices/rules/rendering-activity.md +24 -0
- package/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +38 -0
- package/skills/react-best-practices/rules/rendering-conditional-render.md +32 -0
- package/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/skills/react-best-practices/rules/rendering-hoist-jsx.md +36 -0
- package/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +72 -0
- package/skills/react-best-practices/rules/rendering-hydration-suppress-warning.md +26 -0
- package/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/skills/react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/skills/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/skills/react-best-practices/rules/rerender-functional-setstate.md +77 -0
- package/skills/react-best-practices/rules/rerender-lazy-state-init.md +56 -0
- package/skills/react-best-practices/rules/rerender-memo-with-default-value.md +36 -0
- package/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/skills/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/skills/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/skills/react-best-practices/rules/server-auth-actions.md +96 -0
- package/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/skills/react-best-practices/rules/server-cache-react.md +76 -0
- package/skills/react-best-practices/rules/server-dedup-props.md +65 -0
- package/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/skills/rust/SKILL.md +86 -0
- package/skills/shadcn-ui/SKILL.md +72 -0
- package/skills/skill-builder/SKILL.md +457 -0
- package/skills/skill-builder/checklist.md +65 -0
- package/skills/sqlite/SKILL.md +38 -0
- package/skills/typescript/SKILL.md +453 -0
- package/skills/typescript/assets/eslint-template.js +102 -0
- package/skills/typescript/assets/tsconfig-template.json +45 -0
- package/skills/typescript/references/enterprise-patterns.md +531 -0
- package/skills/typescript/references/generics.md +493 -0
- package/skills/typescript/references/nestjs-integration.md +579 -0
- package/skills/typescript/references/react-integration.md +616 -0
- package/skills/typescript/references/toolchain.md +547 -0
- package/skills/typescript/references/type-system.md +481 -0
- package/skills/vitest-standard/SKILL.md +82 -0
- package/skills/vitest-standard/examples/service-mock.ts +60 -0
- package/skills/vitest-standard/examples/tdd-calculator.ts +41 -0
- package/skills/vitest-standard/examples/type-stubs.d.ts +18 -0
- package/skills/vitest-standard/references/async-and-errors.md +58 -0
- package/skills/vitest-standard/references/coverage-and-config.md +53 -0
- package/skills/vitest-standard/references/mocking.md +61 -0
- package/skills/vitest-standard/references/tdd-patterns.md +60 -0
- package/dist/github-integration-PDRLXKGM.js +0 -1
- package/skills/github-commander/workflows/full-audit.md +0 -134
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
# TypeScript Type System Reference
|
|
2
|
+
|
|
3
|
+
> **Load when:** User asks about type annotations, interfaces vs types, unions, intersections, or type system fundamentals.
|
|
4
|
+
|
|
5
|
+
Complete guide to TypeScript's structural type system.
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- [Type Annotations](#type-annotations)
|
|
10
|
+
- [Interfaces vs Type Aliases](#interfaces-vs-type-aliases)
|
|
11
|
+
- [Union and Intersection Types](#union-and-intersection-types)
|
|
12
|
+
- [Literal Types](#literal-types)
|
|
13
|
+
- [Type Guards and Narrowing](#type-guards-and-narrowing)
|
|
14
|
+
- [The satisfies Operator](#the-satisfies-operator)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Type Annotations
|
|
19
|
+
|
|
20
|
+
### Variable Annotations
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// Explicit type annotations
|
|
24
|
+
const name: string = 'Alice'
|
|
25
|
+
const age: number = 30
|
|
26
|
+
const active: boolean = true
|
|
27
|
+
|
|
28
|
+
// Type inference (preferred when obvious)
|
|
29
|
+
const inferredName = 'Bob' // TypeScript infers string
|
|
30
|
+
const inferredAge = 25 // TypeScript infers number
|
|
31
|
+
|
|
32
|
+
// Arrays
|
|
33
|
+
const numbers: number[] = [1, 2, 3]
|
|
34
|
+
const strings: Array<string> = ['a', 'b', 'c']
|
|
35
|
+
|
|
36
|
+
// Tuples (fixed-length arrays with specific types)
|
|
37
|
+
const pair: [string, number] = ['age', 30]
|
|
38
|
+
const triple: [string, number, boolean] = ['name', 1, true]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Function Annotations
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// Function with typed parameters and return
|
|
45
|
+
function greet(name: string): string {
|
|
46
|
+
return `Hello, ${name}!`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Arrow function
|
|
50
|
+
const add = (a: number, b: number): number => a + b
|
|
51
|
+
|
|
52
|
+
// Optional parameters
|
|
53
|
+
function greetOptional(name: string, greeting?: string): string {
|
|
54
|
+
return `${greeting ?? 'Hello'}, ${name}!`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Default parameters
|
|
58
|
+
function greetDefault(name: string, greeting: string = 'Hello'): string {
|
|
59
|
+
return `${greeting}, ${name}!`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Rest parameters
|
|
63
|
+
function sum(...numbers: number[]): number {
|
|
64
|
+
return numbers.reduce((a, b) => a + b, 0)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Function type alias
|
|
68
|
+
type Comparator<T> = (a: T, b: T) => number
|
|
69
|
+
const numberCompare: Comparator<number> = (a, b) => a - b
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Interfaces vs Type Aliases
|
|
75
|
+
|
|
76
|
+
### When to Use Interfaces
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// Interfaces are ideal for object shapes
|
|
80
|
+
interface User {
|
|
81
|
+
id: string
|
|
82
|
+
name: string
|
|
83
|
+
email: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Interfaces can be extended
|
|
87
|
+
interface Employee extends User {
|
|
88
|
+
employeeId: string
|
|
89
|
+
department: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Interfaces can be implemented by classes
|
|
93
|
+
class Manager implements Employee {
|
|
94
|
+
constructor(
|
|
95
|
+
public id: string,
|
|
96
|
+
public name: string,
|
|
97
|
+
public email: string,
|
|
98
|
+
public employeeId: string,
|
|
99
|
+
public department: string
|
|
100
|
+
) {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Declaration merging (interfaces only)
|
|
104
|
+
interface Config {
|
|
105
|
+
apiUrl: string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface Config {
|
|
109
|
+
timeout: number
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Config now has both apiUrl and timeout
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### When to Use Type Aliases
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// Type aliases for unions
|
|
119
|
+
type Status = 'pending' | 'approved' | 'rejected'
|
|
120
|
+
|
|
121
|
+
// Type aliases for complex types
|
|
122
|
+
type Handler = (event: Event) => void
|
|
123
|
+
|
|
124
|
+
// Type aliases for mapped types
|
|
125
|
+
type Nullable<T> = { [K in keyof T]: T[K] | null }
|
|
126
|
+
|
|
127
|
+
// Type aliases for conditional types
|
|
128
|
+
type NonNullable<T> = T extends null | undefined ? never : T
|
|
129
|
+
|
|
130
|
+
// Type aliases for tuples
|
|
131
|
+
type Point = [x: number, y: number]
|
|
132
|
+
type RGB = [red: number, green: number, blue: number]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Decision Guide
|
|
136
|
+
|
|
137
|
+
| Use Case | Prefer |
|
|
138
|
+
| ----------------- | ----------- |
|
|
139
|
+
| Object shapes | `interface` |
|
|
140
|
+
| Extending objects | `interface` |
|
|
141
|
+
| Class contracts | `interface` |
|
|
142
|
+
| Union types | `type` |
|
|
143
|
+
| Tuple types | `type` |
|
|
144
|
+
| Mapped types | `type` |
|
|
145
|
+
| Conditional types | `type` |
|
|
146
|
+
| Primitive aliases | `type` |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Union and Intersection Types
|
|
151
|
+
|
|
152
|
+
### Union Types
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// Value can be one of several types
|
|
156
|
+
type StringOrNumber = string | number
|
|
157
|
+
|
|
158
|
+
// Discriminated unions (tagged unions)
|
|
159
|
+
interface Dog {
|
|
160
|
+
kind: 'dog'
|
|
161
|
+
bark(): void
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface Cat {
|
|
165
|
+
kind: 'cat'
|
|
166
|
+
meow(): void
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
type Pet = Dog | Cat
|
|
170
|
+
|
|
171
|
+
function speak(pet: Pet): void {
|
|
172
|
+
switch (pet.kind) {
|
|
173
|
+
case 'dog':
|
|
174
|
+
pet.bark()
|
|
175
|
+
break
|
|
176
|
+
case 'cat':
|
|
177
|
+
pet.meow()
|
|
178
|
+
break
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Intersection Types
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// Value must satisfy all types
|
|
187
|
+
interface HasName {
|
|
188
|
+
name: string
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
interface HasAge {
|
|
192
|
+
age: number
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
type Person = HasName & HasAge
|
|
196
|
+
|
|
197
|
+
const person: Person = {
|
|
198
|
+
name: 'Alice',
|
|
199
|
+
age: 30,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Practical: Extending with additional properties
|
|
203
|
+
type WithTimestamp<T> = T & { createdAt: Date; updatedAt: Date }
|
|
204
|
+
|
|
205
|
+
interface Article {
|
|
206
|
+
title: string
|
|
207
|
+
content: string
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
type TimestampedArticle = WithTimestamp<Article>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Literal Types
|
|
216
|
+
|
|
217
|
+
### String Literals
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// Specific string values
|
|
221
|
+
type Direction = 'north' | 'south' | 'east' | 'west'
|
|
222
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
|
223
|
+
|
|
224
|
+
function move(direction: Direction): void {
|
|
225
|
+
console.log(`Moving ${direction}`)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
move('north') // OK
|
|
229
|
+
move('up') // Error: Argument of type '"up"' is not assignable
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Numeric Literals
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6
|
|
236
|
+
type BinaryDigit = 0 | 1
|
|
237
|
+
|
|
238
|
+
function roll(): DiceRoll {
|
|
239
|
+
return Math.ceil(Math.random() * 6) as DiceRoll
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Template Literal Types
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// Construct string literal types
|
|
247
|
+
type EventName = 'click' | 'hover' | 'focus'
|
|
248
|
+
type HandlerName = `on${Capitalize<EventName>}`
|
|
249
|
+
// "onClick" | "onHover" | "onFocus"
|
|
250
|
+
|
|
251
|
+
// CSS unit types
|
|
252
|
+
type CSSUnit = 'px' | 'em' | 'rem' | '%'
|
|
253
|
+
type CSSValue = `${number}${CSSUnit}`
|
|
254
|
+
|
|
255
|
+
const width: CSSValue = '100px' // OK
|
|
256
|
+
const height: CSSValue = '50%' // OK
|
|
257
|
+
const bad: CSSValue = '100' // Error
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Type Guards and Narrowing
|
|
263
|
+
|
|
264
|
+
### Built-in Type Guards
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
function process(value: string | number | null): string {
|
|
268
|
+
// typeof guard
|
|
269
|
+
if (typeof value === 'string') {
|
|
270
|
+
return value.toUpperCase()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// typeof guard for number
|
|
274
|
+
if (typeof value === 'number') {
|
|
275
|
+
return value.toFixed(2)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// null/undefined narrowing
|
|
279
|
+
if (value === null) {
|
|
280
|
+
return 'null'
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Exhaustiveness check
|
|
284
|
+
const _exhaustive: never = value
|
|
285
|
+
throw new Error(`Unhandled case: ${_exhaustive}`)
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### instanceof Guard
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
class ApiError extends Error {
|
|
293
|
+
constructor(
|
|
294
|
+
public statusCode: number,
|
|
295
|
+
message: string
|
|
296
|
+
) {
|
|
297
|
+
super(message)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
class ValidationError extends Error {
|
|
302
|
+
constructor(public fields: string[]) {
|
|
303
|
+
super('Validation failed')
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function handleError(error: Error): void {
|
|
308
|
+
if (error instanceof ApiError) {
|
|
309
|
+
console.log(`API Error ${error.statusCode}: ${error.message}`)
|
|
310
|
+
} else if (error instanceof ValidationError) {
|
|
311
|
+
console.log(`Validation Error on fields: ${error.fields.join(', ')}`)
|
|
312
|
+
} else {
|
|
313
|
+
console.log(`Unknown error: ${error.message}`)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Custom Type Guards
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
interface User {
|
|
322
|
+
type: 'user'
|
|
323
|
+
name: string
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
interface Admin {
|
|
327
|
+
type: 'admin'
|
|
328
|
+
name: string
|
|
329
|
+
permissions: string[]
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
type Account = User | Admin
|
|
333
|
+
|
|
334
|
+
// Type predicate: returns boolean but narrows type
|
|
335
|
+
function isAdmin(account: Account): account is Admin {
|
|
336
|
+
return account.type === 'admin'
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function getPermissions(account: Account): string[] {
|
|
340
|
+
if (isAdmin(account)) {
|
|
341
|
+
return account.permissions // TypeScript knows this is Admin
|
|
342
|
+
}
|
|
343
|
+
return []
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Assertion Functions
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
function assertDefined<T>(value: T | undefined, message: string): asserts value is T {
|
|
351
|
+
if (value === undefined) {
|
|
352
|
+
throw new Error(message)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function processUser(user: User | undefined): void {
|
|
357
|
+
assertDefined(user, 'User is required')
|
|
358
|
+
// After assertion, user is narrowed to User
|
|
359
|
+
console.log(user.name)
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## The satisfies Operator
|
|
366
|
+
|
|
367
|
+
### Problem: Type Assertions Hide Bugs
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
// Using 'as' can hide type errors
|
|
371
|
+
const config = {
|
|
372
|
+
port: 3000,
|
|
373
|
+
host: 'localhost',
|
|
374
|
+
} as Record<string, string | number>
|
|
375
|
+
|
|
376
|
+
// No error, but port is now string | number
|
|
377
|
+
const portString = config.port.toFixed(2) // Runtime error if port is string!
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Solution: satisfies Validates Without Widening
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
// satisfies checks conformance but preserves literal types
|
|
384
|
+
const config = {
|
|
385
|
+
port: 3000,
|
|
386
|
+
host: 'localhost',
|
|
387
|
+
} satisfies Record<string, string | number>
|
|
388
|
+
|
|
389
|
+
// TypeScript knows port is number, host is string
|
|
390
|
+
config.port.toFixed(2) // OK - port is number
|
|
391
|
+
config.host.toUpperCase() // OK - host is string
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Practical Use Cases
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
// Color palette with constrained values
|
|
398
|
+
const palette = {
|
|
399
|
+
primary: '#007bff',
|
|
400
|
+
secondary: '#6c757d',
|
|
401
|
+
success: '#28a745',
|
|
402
|
+
} satisfies Record<string, `#${string}`>
|
|
403
|
+
|
|
404
|
+
// TypeScript knows each property exists and is a hex string
|
|
405
|
+
palette.primary.startsWith('#') // OK
|
|
406
|
+
|
|
407
|
+
// Route configuration
|
|
408
|
+
type RouteConfig = {
|
|
409
|
+
path: string
|
|
410
|
+
method: 'GET' | 'POST'
|
|
411
|
+
handler: () => void
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const routes = {
|
|
415
|
+
home: { path: '/', method: 'GET', handler: () => {} },
|
|
416
|
+
login: { path: '/login', method: 'POST', handler: () => {} },
|
|
417
|
+
} satisfies Record<string, RouteConfig>
|
|
418
|
+
|
|
419
|
+
// TypeScript preserves literal types for each route
|
|
420
|
+
routes.home.method // "GET" (not "GET" | "POST")
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Special Types
|
|
426
|
+
|
|
427
|
+
### any vs unknown
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
// any: Opt out of type checking (avoid)
|
|
431
|
+
let anyValue: any = 'hello'
|
|
432
|
+
anyValue.toFixed(2) // No error, but crashes at runtime
|
|
433
|
+
|
|
434
|
+
// unknown: Type-safe any (prefer)
|
|
435
|
+
let unknownValue: unknown = 'hello'
|
|
436
|
+
unknownValue.toFixed(2) // Error: Object is of type 'unknown'
|
|
437
|
+
|
|
438
|
+
// Must narrow unknown before use
|
|
439
|
+
if (typeof unknownValue === 'string') {
|
|
440
|
+
unknownValue.toUpperCase() // OK after narrowing
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### never
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
// never: Represents impossible values
|
|
448
|
+
function fail(message: string): never {
|
|
449
|
+
throw new Error(message)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Exhaustiveness checking with never
|
|
453
|
+
type Shape = 'circle' | 'square'
|
|
454
|
+
|
|
455
|
+
function getArea(shape: Shape): number {
|
|
456
|
+
switch (shape) {
|
|
457
|
+
case 'circle':
|
|
458
|
+
return Math.PI
|
|
459
|
+
case 'square':
|
|
460
|
+
return 1
|
|
461
|
+
default:
|
|
462
|
+
// If we add a new shape, this will error
|
|
463
|
+
const _exhaustive: never = shape
|
|
464
|
+
throw new Error(`Unknown shape: ${_exhaustive}`)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### void vs undefined
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
// void: Function doesn't return anything meaningful
|
|
473
|
+
function log(message: string): void {
|
|
474
|
+
console.log(message)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// undefined: Explicit undefined value
|
|
478
|
+
function findUser(id: string): User | undefined {
|
|
479
|
+
return users.get(id)
|
|
480
|
+
}
|
|
481
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vitest-standard
|
|
3
|
+
description: |
|
|
4
|
+
Comprehensive unit testing expertise covering Vitest, test-driven
|
|
5
|
+
development (TDD), mocking strategies, and production-grade best practices.
|
|
6
|
+
Activates for unit testing, Vitest, TDD, Red-Green-Refactor, mocking,
|
|
7
|
+
stubbing, spying, test coverage, and test architecture in TypeScript/Node projects.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Vitest Standard
|
|
11
|
+
|
|
12
|
+
This skill provides opinionated, production-tested guidance for high-integrity unit testing with Vitest. It emphasizes behavior-driven design, strictly isolated tests, and the TDD lifecycle.
|
|
13
|
+
|
|
14
|
+
## Golden Rules (Mandatory)
|
|
15
|
+
|
|
16
|
+
1. **Test Behavior, Not Implementation** — Assert what the code _does_, not how it _looks_ internally.
|
|
17
|
+
2. **Strict Isolation** — NO shared state between tests. Create new instances in `beforeEach`.
|
|
18
|
+
3. **Clean Mocks** — Use `vi.clearAllMocks()` in `beforeEach` to prevent call history leaks.
|
|
19
|
+
4. **No Magic Numbers** — Use descriptive variables for expected values.
|
|
20
|
+
5. **AAA Pattern** — Every test must follow Arrange-Act-Assert.
|
|
21
|
+
6. **Async/Await** — Always await promises; use `rejects.toThrow()` for error paths.
|
|
22
|
+
7. **Deterministic Tests** — No dependence on system time, environment variables, or randomness (mock them).
|
|
23
|
+
8. **Meaningful Names** — Use `it('should [action] when [condition]')` or Given-When-Then.
|
|
24
|
+
9. **Mock at Boundaries** — Mock external APIs, databases, and third-party SDKs; test your own logic.
|
|
25
|
+
10. **Red-Green-Refactor** — Prefer writing tests before code to drive API design.
|
|
26
|
+
|
|
27
|
+
## Core Patterns
|
|
28
|
+
|
|
29
|
+
### AAA (Arrange-Act-Assert)
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
it('should calculate total price', () => {
|
|
33
|
+
// Arrange: Setup data and environment
|
|
34
|
+
const cart = new ShoppingCart()
|
|
35
|
+
cart.addItem({ price: 10, quantity: 2 })
|
|
36
|
+
|
|
37
|
+
// Act: Execute the method being tested
|
|
38
|
+
const total = cart.getTotal()
|
|
39
|
+
|
|
40
|
+
// Assert: Verify the outcome
|
|
41
|
+
expect(total).toBe(20)
|
|
42
|
+
})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Mocking Example
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { vi, it, expect } from 'vitest'
|
|
49
|
+
|
|
50
|
+
it('mocks dependencies', () => {
|
|
51
|
+
const mockFn = vi.fn().mockReturnValue(42)
|
|
52
|
+
expect(mockFn()).toBe(42)
|
|
53
|
+
expect(mockFn).toHaveBeenCalledOnce()
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick Reference: Assertions
|
|
58
|
+
|
|
59
|
+
| Assertion | Purpose |
|
|
60
|
+
| :----------------------- | :----------------------------- |
|
|
61
|
+
| `toBe(val)` | Strict equality (`===`) |
|
|
62
|
+
| `toEqual(val)` | Deep equality (objects/arrays) |
|
|
63
|
+
| `toMatchObject(obj)` | Partial match on an object |
|
|
64
|
+
| `toThrow(error?)` | Validates a thrown error |
|
|
65
|
+
| `toHaveBeenCalledWith()` | Verifies mock call arguments |
|
|
66
|
+
| `resolves.toEqual()` | Validates a fulfilled promise |
|
|
67
|
+
| `rejects.toThrow()` | Validates a rejected promise |
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Specialized References (Load On-Demand)
|
|
72
|
+
|
|
73
|
+
| Scenario | Reference File |
|
|
74
|
+
| :--------------------------- | :---------------------------------------------------------- |
|
|
75
|
+
| **Mocking & Doubles** | [mocking.md](references/mocking.md) |
|
|
76
|
+
| **Async & Error Handling** | [async-and-errors.md](references/async-and-errors.md) |
|
|
77
|
+
| **Coverage & Configuration** | [coverage-and-config.md](references/coverage-and-config.md) |
|
|
78
|
+
| **TDD Cycle** | [tdd-patterns.md](references/tdd-patterns.md) |
|
|
79
|
+
|
|
80
|
+
## Example: TDD Calculator
|
|
81
|
+
|
|
82
|
+
See [tdd-calculator.ts](examples/tdd-calculator.ts) for the Red-Green-Refactor workflow.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// Intercept external module
|
|
4
|
+
vi.mock('./api', () => ({
|
|
5
|
+
fetchUser: vi.fn(),
|
|
6
|
+
updateUser: vi.fn(),
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
// Mock instance for Dependency Injection
|
|
10
|
+
const mockDb = {
|
|
11
|
+
save: vi.fn().mockResolvedValue({ id: '123' }),
|
|
12
|
+
delete: vi.fn().mockResolvedValue(true),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('UserService', () => {
|
|
16
|
+
let service: UserService
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
service = new UserService(mockDb as any)
|
|
20
|
+
vi.clearAllMocks() // Clear call history between tests
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.restoreAllMocks() // Restore spies/intercepted methods
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should save user to database', async () => {
|
|
28
|
+
// Arrange
|
|
29
|
+
const user = { name: 'John Doe' }
|
|
30
|
+
|
|
31
|
+
// Act
|
|
32
|
+
const result = await service.saveUser(user)
|
|
33
|
+
|
|
34
|
+
// Assert
|
|
35
|
+
expect(mockDb.save).toHaveBeenCalledOnce()
|
|
36
|
+
expect(mockDb.save).toHaveBeenCalledWith(user)
|
|
37
|
+
expect(result.id).toBe('123')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should throw error for invalid id', async () => {
|
|
41
|
+
// Arrange
|
|
42
|
+
mockDb.save.mockRejectedValueOnce(new Error('Invalid ID'))
|
|
43
|
+
|
|
44
|
+
// Act & Assert (Async Error Path)
|
|
45
|
+
await expect(service.saveUser({ name: '' })).rejects.toThrow('Invalid ID')
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
class UserService {
|
|
50
|
+
constructor(private db: Database) {}
|
|
51
|
+
async saveUser(user: any) {
|
|
52
|
+
return this.db.save(user)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class Database {
|
|
57
|
+
async save(user: any): Promise<any> {
|
|
58
|
+
return { id: '' }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// 1. RED: Write a failing test first
|
|
4
|
+
describe('Calculator', () => {
|
|
5
|
+
it('should add numbers', () => {
|
|
6
|
+
const calc = new Calculator()
|
|
7
|
+
expect(calc.add(2, 3)).toBe(5)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('should multiply numbers', () => {
|
|
11
|
+
const calc = new Calculator()
|
|
12
|
+
expect(calc.multiply(2, 3)).toBe(6)
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// 2. GREEN: Minimal implementations
|
|
17
|
+
class Calculator {
|
|
18
|
+
// Red -> Green -> Refactor cycle
|
|
19
|
+
add(a: number, b: number): number {
|
|
20
|
+
return a + b
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Next iteration:
|
|
24
|
+
multiply(a: number, b: number): number {
|
|
25
|
+
return a * b
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 3. REFACTOR (Example):
|
|
30
|
+
class ImprovedCalculator {
|
|
31
|
+
// Drive the add method with any number of parameters
|
|
32
|
+
add(...numbers: number[]): number {
|
|
33
|
+
return numbers.reduce((sum, n) => sum + n, 0)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 4. Test the refactored version
|
|
38
|
+
it('should add multiple numbers in ImprovedCalculator', () => {
|
|
39
|
+
const calc = new ImprovedCalculator()
|
|
40
|
+
expect(calc.add(1, 2, 3, 4)).toBe(10)
|
|
41
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Type stubs for the Vitest Standard skill examples.
|
|
2
|
+
// This file solves "Cannot find module" errors in the IDE when viewing
|
|
3
|
+
// master reference files outside of a project root.
|
|
4
|
+
|
|
5
|
+
declare module 'vitest' {
|
|
6
|
+
export const describe: any
|
|
7
|
+
export const it: any
|
|
8
|
+
export const expect: any
|
|
9
|
+
export const vi: any
|
|
10
|
+
export const beforeEach: any
|
|
11
|
+
export const afterEach: any
|
|
12
|
+
export const beforeAll: any
|
|
13
|
+
export const afterAll: any
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare module 'vitest/config' {
|
|
17
|
+
export const defineConfig: any
|
|
18
|
+
}
|