@zigrivers/scaffold 3.6.0 → 3.8.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 +127 -12
- package/content/knowledge/backend/backend-api-design.md +103 -0
- package/content/knowledge/backend/backend-architecture.md +100 -0
- package/content/knowledge/backend/backend-async-patterns.md +101 -0
- package/content/knowledge/backend/backend-auth-patterns.md +100 -0
- package/content/knowledge/backend/backend-conventions.md +105 -0
- package/content/knowledge/backend/backend-data-modeling.md +102 -0
- package/content/knowledge/backend/backend-deployment.md +100 -0
- package/content/knowledge/backend/backend-dev-environment.md +102 -0
- package/content/knowledge/backend/backend-observability.md +102 -0
- package/content/knowledge/backend/backend-project-structure.md +100 -0
- package/content/knowledge/backend/backend-requirements.md +103 -0
- package/content/knowledge/backend/backend-security.md +104 -0
- package/content/knowledge/backend/backend-testing.md +101 -0
- package/content/knowledge/backend/backend-worker-patterns.md +100 -0
- package/content/knowledge/cli/cli-architecture.md +101 -0
- package/content/knowledge/cli/cli-conventions.md +117 -0
- package/content/knowledge/cli/cli-dev-environment.md +121 -0
- package/content/knowledge/cli/cli-distribution-patterns.md +106 -0
- package/content/knowledge/cli/cli-interactivity-patterns.md +116 -0
- package/content/knowledge/cli/cli-output-patterns.md +107 -0
- package/content/knowledge/cli/cli-project-structure.md +124 -0
- package/content/knowledge/cli/cli-requirements.md +101 -0
- package/content/knowledge/cli/cli-shell-integration.md +130 -0
- package/content/knowledge/cli/cli-testing.md +134 -0
- package/content/knowledge/library/library-api-design.md +306 -0
- package/content/knowledge/library/library-architecture.md +247 -0
- package/content/knowledge/library/library-bundling.md +244 -0
- package/content/knowledge/library/library-conventions.md +229 -0
- package/content/knowledge/library/library-dev-environment.md +220 -0
- package/content/knowledge/library/library-documentation.md +300 -0
- package/content/knowledge/library/library-project-structure.md +237 -0
- package/content/knowledge/library/library-requirements.md +173 -0
- package/content/knowledge/library/library-security.md +257 -0
- package/content/knowledge/library/library-testing.md +319 -0
- package/content/knowledge/library/library-type-definitions.md +284 -0
- package/content/knowledge/library/library-versioning.md +300 -0
- package/content/knowledge/mobile-app/mobile-app-architecture.md +283 -0
- package/content/knowledge/mobile-app/mobile-app-conventions.md +180 -0
- package/content/knowledge/mobile-app/mobile-app-deployment.md +298 -0
- package/content/knowledge/mobile-app/mobile-app-dev-environment.md +257 -0
- package/content/knowledge/mobile-app/mobile-app-distribution.md +264 -0
- package/content/knowledge/mobile-app/mobile-app-observability.md +317 -0
- package/content/knowledge/mobile-app/mobile-app-offline-patterns.md +311 -0
- package/content/knowledge/mobile-app/mobile-app-project-structure.md +245 -0
- package/content/knowledge/mobile-app/mobile-app-push-notifications.md +321 -0
- package/content/knowledge/mobile-app/mobile-app-requirements.md +147 -0
- package/content/knowledge/mobile-app/mobile-app-security.md +338 -0
- package/content/knowledge/mobile-app/mobile-app-testing.md +400 -0
- package/content/knowledge/web-app/web-app-api-patterns.md +224 -0
- package/content/knowledge/web-app/web-app-architecture.md +116 -0
- package/content/knowledge/web-app/web-app-auth-patterns.md +256 -0
- package/content/knowledge/web-app/web-app-conventions.md +121 -0
- package/content/knowledge/web-app/web-app-data-patterns.md +218 -0
- package/content/knowledge/web-app/web-app-deployment-workflow.md +143 -0
- package/content/knowledge/web-app/web-app-deployment.md +134 -0
- package/content/knowledge/web-app/web-app-design-system.md +158 -0
- package/content/knowledge/web-app/web-app-dev-environment.md +173 -0
- package/content/knowledge/web-app/web-app-observability.md +221 -0
- package/content/knowledge/web-app/web-app-project-structure.md +160 -0
- package/content/knowledge/web-app/web-app-rendering-strategies.md +133 -0
- package/content/knowledge/web-app/web-app-requirements.md +112 -0
- package/content/knowledge/web-app/web-app-security.md +193 -0
- package/content/knowledge/web-app/web-app-session-patterns.md +214 -0
- package/content/knowledge/web-app/web-app-testing.md +249 -0
- package/content/knowledge/web-app/web-app-ux-patterns.md +162 -0
- package/content/methodology/backend-overlay.yml +73 -0
- package/content/methodology/cli-overlay.yml +69 -0
- package/content/methodology/library-overlay.yml +67 -0
- package/content/methodology/mobile-app-overlay.yml +71 -0
- package/content/methodology/web-app-overlay.yml +79 -0
- package/dist/cli/commands/init.d.ts +21 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +261 -13
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +206 -0
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/config/schema.d.ts +1392 -64
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +82 -5
- package/dist/config/schema.js.map +1 -1
- package/dist/config/schema.test.js +302 -1
- package/dist/config/schema.test.js.map +1 -1
- package/dist/core/assembly/overlay-loader.d.ts.map +1 -1
- package/dist/core/assembly/overlay-loader.js +2 -1
- package/dist/core/assembly/overlay-loader.js.map +1 -1
- package/dist/core/assembly/overlay-loader.test.js +56 -0
- package/dist/core/assembly/overlay-loader.test.js.map +1 -1
- package/dist/e2e/game-pipeline.test.js +1 -0
- package/dist/e2e/game-pipeline.test.js.map +1 -1
- package/dist/e2e/project-type-overlays.test.d.ts +16 -0
- package/dist/e2e/project-type-overlays.test.d.ts.map +1 -0
- package/dist/e2e/project-type-overlays.test.js +834 -0
- package/dist/e2e/project-type-overlays.test.js.map +1 -0
- package/dist/types/config.d.ts +19 -2
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -1
- package/dist/types/index.js.map +1 -1
- package/dist/wizard/questions.d.ts +27 -1
- package/dist/wizard/questions.d.ts.map +1 -1
- package/dist/wizard/questions.js +142 -3
- package/dist/wizard/questions.js.map +1 -1
- package/dist/wizard/questions.test.js +206 -8
- package/dist/wizard/questions.test.js.map +1 -1
- package/dist/wizard/wizard.d.ts +21 -0
- package/dist/wizard/wizard.d.ts.map +1 -1
- package/dist/wizard/wizard.js +27 -1
- package/dist/wizard/wizard.js.map +1 -1
- package/package.json +1 -1
- package/dist/types/wizard.d.ts +0 -14
- package/dist/types/wizard.d.ts.map +0 -1
- package/dist/types/wizard.js +0 -2
- package/dist/types/wizard.js.map +0 -1
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: library-testing
|
|
3
|
+
description: Unit tests, consumer integration tests, example app tests, snapshot testing, and type testing for published libraries
|
|
4
|
+
topics: [library, testing, unit-tests, integration-tests, type-testing, tsd, snapshot-testing, consumer-testing]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Library testing has a different threat model than application testing. You are not just testing that your code works internally — you are testing that the public API works correctly from a consumer's perspective, across different environments and module systems, and that type definitions accurately reflect runtime behavior. A library with excellent unit tests but untested consumer integration can still fail spectacularly when installed in a real project. Every public API export needs test coverage that exercises it as a consumer would.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Library testing requires four layers: unit tests for internal logic (fast, isolated), consumer-perspective integration tests (exercise the public API as an external consumer would), example application tests (run the examples from the docs to catch regressions), and type tests (verify TypeScript types are accurate and don't regress). Use vitest for unit and integration tests. Use `tsd` or vitest's `expectTypeOf` for type tests. Run example apps in CI. Aim for 100% coverage of public API exports.
|
|
12
|
+
|
|
13
|
+
Testing layers:
|
|
14
|
+
- Unit tests: pure functions, internal logic, error conditions
|
|
15
|
+
- Integration tests: public API called without any internal access
|
|
16
|
+
- Type tests: `tsd` or `expect-type` assertions on all public exports
|
|
17
|
+
- Example tests: run examples/ as standalone scripts in CI
|
|
18
|
+
- Cross-environment tests: Node ESM, Node CJS, and bundler environments
|
|
19
|
+
|
|
20
|
+
## Deep Guidance
|
|
21
|
+
|
|
22
|
+
### Unit Tests
|
|
23
|
+
|
|
24
|
+
Unit tests cover individual functions and classes in isolation. Use vitest for its TypeScript support and fast execution:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// tests/unit/parser.test.ts
|
|
28
|
+
import { describe, it, expect } from 'vitest'
|
|
29
|
+
import { parseConfig } from '../../src/parser'
|
|
30
|
+
import { ParseError } from '../../src/errors'
|
|
31
|
+
|
|
32
|
+
describe('parseConfig', () => {
|
|
33
|
+
it('parses a valid TOML string', () => {
|
|
34
|
+
const result = parseConfig('[server]\nhost = "localhost"\nport = 3000')
|
|
35
|
+
expect(result.server.host).toBe('localhost')
|
|
36
|
+
expect(result.server.port).toBe(3000)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('throws ParseError on invalid TOML', () => {
|
|
40
|
+
expect(() => parseConfig('invalid = {toml')).toThrow(ParseError)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('includes line number in ParseError', () => {
|
|
44
|
+
try {
|
|
45
|
+
parseConfig('line1 = "ok"\nline2 = {invalid')
|
|
46
|
+
} catch (err) {
|
|
47
|
+
expect(err).toBeInstanceOf(ParseError)
|
|
48
|
+
expect((err as ParseError).line).toBe(2)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('returns empty config for empty input', () => {
|
|
53
|
+
const result = parseConfig('')
|
|
54
|
+
expect(result).toEqual({})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('handles special characters in string values', () => {
|
|
58
|
+
const result = parseConfig('key = "hello\\nworld"')
|
|
59
|
+
expect(result.key).toBe('hello\nworld')
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Unit test coverage targets:**
|
|
65
|
+
- Every exported function: 100% coverage
|
|
66
|
+
- Every error case: tested explicitly
|
|
67
|
+
- Edge cases: empty inputs, null/undefined, very large inputs, special characters
|
|
68
|
+
- Option combinations: test that each option modifies behavior correctly
|
|
69
|
+
|
|
70
|
+
### Consumer-Perspective Integration Tests
|
|
71
|
+
|
|
72
|
+
These tests exercise the public API exactly as a consumer would — importing from the package root, never accessing internal modules:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// tests/integration/consumer-api.test.ts
|
|
76
|
+
// Import from the package root, not from src/
|
|
77
|
+
// This tests the actual public API surface
|
|
78
|
+
import { parseConfig, createClient, ParseError, ValidationError } from 'my-library'
|
|
79
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
80
|
+
|
|
81
|
+
// If running in Node, import the dist/ version not the source
|
|
82
|
+
// Configure vitest to resolve 'my-library' to './dist/index.js'
|
|
83
|
+
|
|
84
|
+
describe('Consumer API', () => {
|
|
85
|
+
describe('parseConfig()', () => {
|
|
86
|
+
it('is callable with a single string argument', () => {
|
|
87
|
+
expect(() => parseConfig('[test]\nvalue = 1')).not.toThrow()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('returns an object with the parsed structure', () => {
|
|
91
|
+
const config = parseConfig('[db]\nhost = "localhost"')
|
|
92
|
+
expect(config).toMatchObject({ db: { host: 'localhost' } })
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('throws ParseError (a named export) on syntax errors', () => {
|
|
96
|
+
expect(() => parseConfig('invalid')).toThrow(ParseError)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('createClient()', () => {
|
|
101
|
+
let client: ReturnType<typeof createClient>
|
|
102
|
+
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
const config = parseConfig('[server]\nhost = "localhost"\nport = 3000')
|
|
105
|
+
client = createClient({ config })
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
afterEach(async () => {
|
|
109
|
+
await client.close()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('creates a client with a query method', () => {
|
|
113
|
+
expect(client.query).toBeTypeOf('function')
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Configure vitest to test against dist/ for integration tests:**
|
|
120
|
+
```typescript
|
|
121
|
+
// vitest.config.ts
|
|
122
|
+
import { defineConfig } from 'vitest'
|
|
123
|
+
|
|
124
|
+
export default defineConfig({
|
|
125
|
+
test: {
|
|
126
|
+
// Unit tests use src/ directly
|
|
127
|
+
// Integration tests use the built dist/
|
|
128
|
+
include: ['tests/unit/**/*.test.ts'],
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// vitest.integration.config.ts
|
|
133
|
+
export default defineConfig({
|
|
134
|
+
resolve: {
|
|
135
|
+
alias: {
|
|
136
|
+
'my-library': new URL('./dist/index.js', import.meta.url).pathname
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
test: {
|
|
140
|
+
include: ['tests/integration/**/*.test.ts'],
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Type Tests with tsd
|
|
146
|
+
|
|
147
|
+
Type tests verify that TypeScript types are accurate and don't regress:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// tests/types/index.test-d.ts
|
|
151
|
+
import { expectType, expectError, expectAssignable, expectNotType } from 'tsd'
|
|
152
|
+
import type { Config, ParseOptions, ParseError as PE } from 'my-library'
|
|
153
|
+
import { parseConfig, createClient, ParseError } from 'my-library'
|
|
154
|
+
|
|
155
|
+
// Return type tests
|
|
156
|
+
expectType<Config>(parseConfig('[test]\nvalue = 1'))
|
|
157
|
+
|
|
158
|
+
// Overload resolution
|
|
159
|
+
expectType<Config>(parseConfig('input', { async: false }))
|
|
160
|
+
expectType<Promise<Config>>(parseConfig('input', { async: true }))
|
|
161
|
+
|
|
162
|
+
// Input type constraints
|
|
163
|
+
expectError(parseConfig(42))
|
|
164
|
+
expectError(parseConfig(null))
|
|
165
|
+
expectError(parseConfig(undefined))
|
|
166
|
+
expectError(parseConfig([]))
|
|
167
|
+
|
|
168
|
+
// Options shape
|
|
169
|
+
const validOptions: ParseOptions = { strict: true, encoding: 'utf-8' }
|
|
170
|
+
expectAssignable<ParseOptions>(validOptions)
|
|
171
|
+
expectAssignable<ParseOptions>({}) // All options are optional
|
|
172
|
+
|
|
173
|
+
// Error type hierarchy
|
|
174
|
+
const err = new ParseError('msg', 1, 1)
|
|
175
|
+
expectAssignable<Error>(err)
|
|
176
|
+
expectType<number>(err.line)
|
|
177
|
+
expectType<number>(err.column)
|
|
178
|
+
|
|
179
|
+
// Discriminated union types
|
|
180
|
+
type ParseResult = ReturnType<typeof parseConfig>
|
|
181
|
+
expectNotType<undefined>(null as ParseResult) // Result is never null
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
// package.json — configure tsd
|
|
186
|
+
{
|
|
187
|
+
"tsd": {
|
|
188
|
+
"directory": "tests/types"
|
|
189
|
+
},
|
|
190
|
+
"scripts": {
|
|
191
|
+
"test:types": "tsd"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Snapshot Testing for Serialized Output
|
|
197
|
+
|
|
198
|
+
Snapshot tests catch unintentional changes to formatted output:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// tests/unit/formatter.test.ts
|
|
202
|
+
import { describe, it, expect } from 'vitest'
|
|
203
|
+
import { formatConfig } from '../../src/formatter'
|
|
204
|
+
|
|
205
|
+
describe('formatConfig', () => {
|
|
206
|
+
it('formats a config object to TOML', () => {
|
|
207
|
+
const config = { server: { host: 'localhost', port: 3000 } }
|
|
208
|
+
expect(formatConfig(config)).toMatchSnapshot()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('formats an array value', () => {
|
|
212
|
+
const config = { allowed: ['read', 'write'] }
|
|
213
|
+
expect(formatConfig(config)).toMatchSnapshot()
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Snapshot files are committed to the repository. When intentional output changes are made, update snapshots with `vitest --update-snapshots`. In CI, fail on unexpected snapshot changes.
|
|
219
|
+
|
|
220
|
+
**When to use snapshots vs. explicit assertions:**
|
|
221
|
+
- Use snapshots for complex serialized output (HTML, TOML, JSON with many fields)
|
|
222
|
+
- Use explicit assertions for simple return values — snapshots hide intent
|
|
223
|
+
- Never snapshot non-deterministic output (timestamps, random IDs)
|
|
224
|
+
|
|
225
|
+
### Cross-Environment Testing
|
|
226
|
+
|
|
227
|
+
Libraries must work in both Node ESM and CJS environments:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
// tests/env/esm.test.mjs — ES module import test
|
|
231
|
+
import { parseConfig } from 'my-library'
|
|
232
|
+
console.assert(typeof parseConfig === 'function', 'ESM import works')
|
|
233
|
+
console.log('ESM: OK')
|
|
234
|
+
|
|
235
|
+
// tests/env/cjs.test.cjs — CommonJS require test
|
|
236
|
+
const { parseConfig } = require('my-library')
|
|
237
|
+
console.assert(typeof parseConfig === 'function', 'CJS require works')
|
|
238
|
+
console.log('CJS: OK')
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
```yaml
|
|
242
|
+
# .github/workflows/ci.yml
|
|
243
|
+
- name: Test CJS compatibility
|
|
244
|
+
run: node tests/env/cjs.test.cjs
|
|
245
|
+
|
|
246
|
+
- name: Test ESM compatibility
|
|
247
|
+
run: node tests/env/esm.test.mjs
|
|
248
|
+
|
|
249
|
+
- name: Test with latest Node
|
|
250
|
+
run: npm test
|
|
251
|
+
env:
|
|
252
|
+
NODE_VERSION: 22
|
|
253
|
+
|
|
254
|
+
- name: Test with minimum Node
|
|
255
|
+
run: npm test
|
|
256
|
+
env:
|
|
257
|
+
NODE_VERSION: 18
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Example Application Tests
|
|
261
|
+
|
|
262
|
+
Run examples in CI to catch regressions in the documented usage:
|
|
263
|
+
|
|
264
|
+
```yaml
|
|
265
|
+
# .github/workflows/ci.yml
|
|
266
|
+
- name: Build library
|
|
267
|
+
run: npm run build
|
|
268
|
+
|
|
269
|
+
- name: Test basic-usage example
|
|
270
|
+
run: |
|
|
271
|
+
cd examples/basic-usage
|
|
272
|
+
npm install
|
|
273
|
+
node index.js
|
|
274
|
+
# Fails if the example throws an error or exits non-zero
|
|
275
|
+
|
|
276
|
+
- name: Test advanced example
|
|
277
|
+
run: |
|
|
278
|
+
cd examples/advanced-plugin
|
|
279
|
+
npm install
|
|
280
|
+
node index.js
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Each example's `package.json` references `"my-library": "file:../../"` so it uses the locally built dist/, not a published version:
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
{
|
|
287
|
+
"dependencies": {
|
|
288
|
+
"my-library": "file:../../"
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
If an example breaks, the public API has regressed. Fix the library, not the example.
|
|
294
|
+
|
|
295
|
+
### Test Coverage Configuration
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// vitest.config.ts
|
|
299
|
+
import { defineConfig } from 'vitest'
|
|
300
|
+
|
|
301
|
+
export default defineConfig({
|
|
302
|
+
test: {
|
|
303
|
+
coverage: {
|
|
304
|
+
provider: 'v8',
|
|
305
|
+
include: ['src/**/*.ts'],
|
|
306
|
+
exclude: ['src/internal/**', 'src/**/*.test.ts'],
|
|
307
|
+
thresholds: {
|
|
308
|
+
functions: 100, // Every exported function must be tested
|
|
309
|
+
branches: 90, // 90% branch coverage
|
|
310
|
+
lines: 95,
|
|
311
|
+
statements: 95
|
|
312
|
+
},
|
|
313
|
+
reporter: ['text', 'lcov', 'html']
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
A 100% function coverage target is achievable for libraries and enforces that every public export has at least one test. Combined with type tests (which test every function's type signature), this gives comprehensive coverage of the public API contract.
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: library-type-definitions
|
|
3
|
+
description: Declaration files (.d.ts), type testing with tsd/expect-type, conditional types, and API surface documentation via types
|
|
4
|
+
topics: [library, typescript, type-definitions, declarations, tsd, expect-type, conditional-types, dts]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Type definitions are a first-class deliverable for TypeScript libraries. They are part of the public API contract, not an afterthought. Declaration files (`.d.ts`) must be accurate, complete, and expressive — they determine whether consumers can use the library with type safety, whether IDEs provide useful completions, and whether type errors are caught at compile time rather than runtime. Inaccurate types are worse than no types because they create false confidence.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Emit declaration files alongside compiled JavaScript using TypeScript's `declaration: true` compiler option or a bundler like tsup. Test types with `tsd` or `expect-type` to catch type regressions. Use conditional types to express relationships between input and output types. Export all types that appear in public API signatures — unexported types force consumers to use `any` or `ReturnType<typeof fn>` workarounds. Document type parameters with JSDoc. Include declaration maps (`declarationMap: true`) so Go-to-Definition works across the source.
|
|
12
|
+
|
|
13
|
+
Core type definition practices:
|
|
14
|
+
- Enable `declaration` and `declarationMap` in tsconfig build config
|
|
15
|
+
- Export every type that appears in any public API signature
|
|
16
|
+
- Write type tests with `tsd` or `expect-type` as part of the test suite
|
|
17
|
+
- Use conditional types to narrow return types based on input types
|
|
18
|
+
- Avoid `any` in public signatures — use `unknown` for unvalidated input
|
|
19
|
+
|
|
20
|
+
## Deep Guidance
|
|
21
|
+
|
|
22
|
+
### TypeScript Configuration for Declaration Emission
|
|
23
|
+
|
|
24
|
+
The build tsconfig must enable declaration emission:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"compilerOptions": {
|
|
29
|
+
"declaration": true, // Emit .d.ts files
|
|
30
|
+
"declarationMap": true, // Emit .d.ts.map for Go-to-Definition
|
|
31
|
+
"declarationDir": "./dist/types", // Put declarations in dist/types/
|
|
32
|
+
"emitDeclarationOnly": false, // Emit JS too (or use separate tsc runs)
|
|
33
|
+
"stripInternal": true, // Remove @internal JSDoc items from declarations
|
|
34
|
+
"strict": true,
|
|
35
|
+
"noUncheckedIndexedAccess": true,
|
|
36
|
+
"exactOptionalPropertyTypes": true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
With tsup:
|
|
42
|
+
```typescript
|
|
43
|
+
export default defineConfig({
|
|
44
|
+
entry: ['src/index.ts'],
|
|
45
|
+
dts: true, // tsup generates declarations using tsc under the hood
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### What to Export
|
|
50
|
+
|
|
51
|
+
Every type that appears in a public API signature must be exported:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// src/index.ts — export all types consumers will need
|
|
55
|
+
|
|
56
|
+
// Types used in function signatures
|
|
57
|
+
export type { ParseOptions, ParseResult } from './parser'
|
|
58
|
+
export type { ValidationResult, ValidationError, ValidationRule } from './validator'
|
|
59
|
+
export type { ClientOptions, Client, RequestOptions, Response } from './client'
|
|
60
|
+
|
|
61
|
+
// Error classes (constructors are values AND types)
|
|
62
|
+
export { ParseError, ValidationError, NetworkError } from './errors'
|
|
63
|
+
|
|
64
|
+
// Enums or const objects used as parameter values
|
|
65
|
+
export { LogLevel, OutputFormat } from './constants'
|
|
66
|
+
|
|
67
|
+
// Utility types consumers may want to use
|
|
68
|
+
export type { DeepPartial, Awaited } from './utils'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**What NOT to export:**
|
|
72
|
+
- Internal implementation types (`InternalCacheEntry`, `HttpClientConfig`)
|
|
73
|
+
- Types used only within the library's own internals
|
|
74
|
+
- Types from `src/internal/` modules
|
|
75
|
+
|
|
76
|
+
Mark internal types with `@internal` JSDoc to exclude them from declaration files when `stripInternal: true` is set:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
/** @internal */
|
|
80
|
+
export interface InternalCacheEntry {
|
|
81
|
+
value: unknown
|
|
82
|
+
expiresAt: number
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Type Testing with tsd
|
|
87
|
+
|
|
88
|
+
`tsd` lets you write assertions about types as test files. These catch type regressions — when a refactor accidentally changes the type of a public API:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// tests/types/index.test-d.ts
|
|
92
|
+
import { expectType, expectError, expectAssignable } from 'tsd'
|
|
93
|
+
import { parseConfig, ParseOptions, ParseError, Config } from 'my-library'
|
|
94
|
+
|
|
95
|
+
// Assert parseConfig returns Config
|
|
96
|
+
expectType<Config>(parseConfig('input'))
|
|
97
|
+
|
|
98
|
+
// Assert ParseOptions is accepted
|
|
99
|
+
const options: ParseOptions = { strict: true }
|
|
100
|
+
expectAssignable<ParseOptions>(options)
|
|
101
|
+
|
|
102
|
+
// Assert ParseError has the right shape
|
|
103
|
+
const err = new ParseError('message', 1, 1)
|
|
104
|
+
expectType<number>(err.line)
|
|
105
|
+
expectType<number>(err.column)
|
|
106
|
+
|
|
107
|
+
// Assert that invalid input types cause a type error
|
|
108
|
+
expectError(parseConfig(42))
|
|
109
|
+
expectError(parseConfig(null))
|
|
110
|
+
|
|
111
|
+
// Assert overload resolution
|
|
112
|
+
expectType<Config>(parseConfig('input'))
|
|
113
|
+
expectType<Promise<Config>>(parseConfig('input', { async: true }))
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Run type tests as part of CI:
|
|
117
|
+
```bash
|
|
118
|
+
# package.json
|
|
119
|
+
"test:types": "tsd"
|
|
120
|
+
"test": "vitest run && tsd"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`tsd` will fail the test suite if any `expectType` assertion fails (wrong type) or any `expectError` assertion fails (expected error didn't occur).
|
|
124
|
+
|
|
125
|
+
### Using expect-type (Alternative to tsd)
|
|
126
|
+
|
|
127
|
+
`expect-type` works inline with your test runner (vitest, jest):
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// tests/types/parser.test.ts
|
|
131
|
+
import { expectTypeOf } from 'vitest'
|
|
132
|
+
import { parseConfig } from 'my-library'
|
|
133
|
+
|
|
134
|
+
test('parseConfig return type', () => {
|
|
135
|
+
expectTypeOf(parseConfig).toBeFunction()
|
|
136
|
+
expectTypeOf(parseConfig).parameter(0).toBeString()
|
|
137
|
+
expectTypeOf(parseConfig).returns.toEqualTypeOf<Config>()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('ParseOptions is a valid options object', () => {
|
|
141
|
+
expectTypeOf<ParseOptions>().toHaveProperty('strict')
|
|
142
|
+
expectTypeOf<ParseOptions['strict']>().toEqualTypeOf<boolean | undefined>()
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The advantage of `expect-type` within vitest: type tests live alongside runtime tests and run in the same test command. The advantage of `tsd`: it's a dedicated type testing tool with more ergonomic syntax for complex assertions.
|
|
147
|
+
|
|
148
|
+
### Conditional Types for Precise Return Types
|
|
149
|
+
|
|
150
|
+
Conditional types allow the return type to depend on the input type:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Return type narrows based on whether async option is set
|
|
154
|
+
function parseConfig(input: string, options?: { async?: false }): Config
|
|
155
|
+
function parseConfig(input: string, options: { async: true }): Promise<Config>
|
|
156
|
+
function parseConfig(input: string, options?: { async?: boolean }): Config | Promise<Config>
|
|
157
|
+
|
|
158
|
+
// Generic conditional type
|
|
159
|
+
type ParseResult<T extends ParseOptions> =
|
|
160
|
+
T extends { async: true } ? Promise<Config> : Config
|
|
161
|
+
|
|
162
|
+
// Deep conditional: extract value type from Result
|
|
163
|
+
type Unwrap<T> = T extends Promise<infer U> ? U : T
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Practical conditional type patterns:**
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Input determines output type
|
|
170
|
+
function transform<T extends string | Buffer>(
|
|
171
|
+
input: T
|
|
172
|
+
): T extends string ? string : Buffer
|
|
173
|
+
|
|
174
|
+
// Stricter types in strict mode
|
|
175
|
+
interface ParseOptions {
|
|
176
|
+
strict?: boolean
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
type ParseReturn<T extends ParseOptions> =
|
|
180
|
+
T extends { strict: true } ? StrictConfig : Config
|
|
181
|
+
|
|
182
|
+
// Utility: make deeply optional
|
|
183
|
+
type DeepPartial<T> = {
|
|
184
|
+
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Use conditional types when the return type genuinely depends on the input. Don't overuse them — complex conditional types are hard to debug and can produce confusing error messages.
|
|
189
|
+
|
|
190
|
+
### Declaration Maps
|
|
191
|
+
|
|
192
|
+
Declaration maps (`.d.ts.map`) enable Go-to-Definition in IDEs to jump to the TypeScript source rather than the compiled declaration file. This dramatically improves the experience of debugging library code:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
// tsconfig.json
|
|
196
|
+
{
|
|
197
|
+
"compilerOptions": {
|
|
198
|
+
"declarationMap": true // Emits .d.ts.map alongside .d.ts
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Include the source maps in the published package:
|
|
204
|
+
```json
|
|
205
|
+
// package.json
|
|
206
|
+
{
|
|
207
|
+
"files": [
|
|
208
|
+
"dist/", // includes .d.ts.map files
|
|
209
|
+
"src/", // include source for source map resolution
|
|
210
|
+
"README.md"
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Including `src/` in the published package allows IDEs to resolve the original TypeScript source through the declaration map.
|
|
216
|
+
|
|
217
|
+
### Template Literal Types for String APIs
|
|
218
|
+
|
|
219
|
+
When a library accepts string patterns, template literal types provide exact-match checking:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// HTTP method type
|
|
223
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'
|
|
224
|
+
|
|
225
|
+
// Route parameter extraction
|
|
226
|
+
type ExtractParams<Route extends string> =
|
|
227
|
+
Route extends `${string}:${infer Param}/${infer Rest}`
|
|
228
|
+
? Param | ExtractParams<`/${Rest}`>
|
|
229
|
+
: Route extends `${string}:${infer Param}`
|
|
230
|
+
? Param
|
|
231
|
+
: never
|
|
232
|
+
|
|
233
|
+
// Usage:
|
|
234
|
+
type Params = ExtractParams<'/users/:userId/posts/:postId'>
|
|
235
|
+
// Type: "userId" | "postId"
|
|
236
|
+
|
|
237
|
+
function route<R extends string>(
|
|
238
|
+
path: R,
|
|
239
|
+
handler: (params: Record<ExtractParams<R>, string>) => void
|
|
240
|
+
): void
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Template literal types are powerful for configuration DSLs and string-based APIs. Use them when the consumer's strings have a predictable structure that TypeScript can validate.
|
|
244
|
+
|
|
245
|
+
### JSDoc for Type Documentation
|
|
246
|
+
|
|
247
|
+
TypeDoc and IDE hover text both read JSDoc comments from type declarations:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
/**
|
|
251
|
+
* Options for controlling the parsing behavior.
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```typescript
|
|
255
|
+
* const config = parseConfig(input, {
|
|
256
|
+
* strict: true,
|
|
257
|
+
* encoding: 'utf-8'
|
|
258
|
+
* })
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
export interface ParseOptions {
|
|
262
|
+
/**
|
|
263
|
+
* Enable strict mode validation.
|
|
264
|
+
* In strict mode, unknown fields cause a ValidationError.
|
|
265
|
+
* @default false
|
|
266
|
+
*/
|
|
267
|
+
strict?: boolean
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* File encoding for file-based parsing.
|
|
271
|
+
* @default 'utf-8'
|
|
272
|
+
*/
|
|
273
|
+
encoding?: BufferEncoding
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Maximum input size in bytes.
|
|
277
|
+
* Inputs larger than this limit throw a ParseError.
|
|
278
|
+
* @default 1_048_576 (1 MB)
|
|
279
|
+
*/
|
|
280
|
+
maxSize?: number
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
JSDoc on interfaces and their properties generates rich documentation and appears in IDE hover. Always document: the purpose, any constraints, and the default value.
|