litclaude-ai 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +155 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/README_ko-KR.md +374 -0
- package/RELEASE_CHECKLIST.md +165 -0
- package/bin/litclaude-ai.js +643 -0
- package/cover.png +0 -0
- package/docs/agents.md +67 -0
- package/docs/hooks.md +134 -0
- package/docs/lsp.md +40 -0
- package/docs/migration.md +209 -0
- package/docs/workflow-compatibility-audit.md +119 -0
- package/generate_cover.py +123 -0
- package/package.json +48 -0
- package/plugins/litclaude/.claude-plugin/plugin.json +25 -0
- package/plugins/litclaude/.lsp.json +13 -0
- package/plugins/litclaude/.mcp.json +9 -0
- package/plugins/litclaude/agents/boulder-executor.md +12 -0
- package/plugins/litclaude/agents/librarian-researcher.md +15 -0
- package/plugins/litclaude/agents/oracle-verifier.md +16 -0
- package/plugins/litclaude/agents/prometheus-planner.md +13 -0
- package/plugins/litclaude/agents/qa-runner.md +16 -0
- package/plugins/litclaude/agents/quality-reviewer.md +17 -0
- package/plugins/litclaude/bin/litclaude-hook.js +110 -0
- package/plugins/litclaude/bin/litclaude-hud.js +271 -0
- package/plugins/litclaude/bin/litclaude-lsp-doctor.js +15 -0
- package/plugins/litclaude/bin/litclaude-mcp.js +70 -0
- package/plugins/litclaude/commands/deep-interview.md +21 -0
- package/plugins/litclaude/commands/dynamic-workflow.md +36 -0
- package/plugins/litclaude/commands/lit-loop.md +40 -0
- package/plugins/litclaude/commands/lit-plan.md +35 -0
- package/plugins/litclaude/commands/litgoal.md +30 -0
- package/plugins/litclaude/commands/review-work.md +35 -0
- package/plugins/litclaude/commands/start-work.md +36 -0
- package/plugins/litclaude/hooks/hooks.json +54 -0
- package/plugins/litclaude/lib/context-pressure.mjs +25 -0
- package/plugins/litclaude/lib/hud-accent-palette.mjs +58 -0
- package/plugins/litclaude/lib/litgoal/cli.mjs +266 -0
- package/plugins/litclaude/lib/litgoal/ledger.mjs +16 -0
- package/plugins/litclaude/lib/litgoal/paths.mjs +7 -0
- package/plugins/litclaude/lib/litgoal/state.mjs +67 -0
- package/plugins/litclaude/lib/mutated-file-paths.mjs +63 -0
- package/plugins/litclaude/lib/start-work-continuation.mjs +99 -0
- package/plugins/litclaude/lib/workflow-check.mjs +83 -0
- package/plugins/litclaude/skills/ai-slop-remover/SKILL.md +142 -0
- package/plugins/litclaude/skills/comment-checker/SKILL.md +55 -0
- package/plugins/litclaude/skills/debugging/SKILL.md +70 -0
- package/plugins/litclaude/skills/debugging/references/methodology/00-setup.md +108 -0
- package/plugins/litclaude/skills/debugging/references/methodology/02-investigate.md +126 -0
- package/plugins/litclaude/skills/debugging/references/methodology/04-oracle-triple.md +106 -0
- package/plugins/litclaude/skills/debugging/references/methodology/05-escalate.md +69 -0
- package/plugins/litclaude/skills/debugging/references/methodology/06-fix.md +116 -0
- package/plugins/litclaude/skills/debugging/references/methodology/08-qa.md +94 -0
- package/plugins/litclaude/skills/debugging/references/methodology/09-cleanup.md +164 -0
- package/plugins/litclaude/skills/debugging/references/methodology/partial-runtime-evidence.md +228 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/go.md +252 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/native-binary.md +484 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/node.md +260 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/python.md +248 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/rust.md +234 -0
- package/plugins/litclaude/skills/debugging/references/tools/ghidra.md +212 -0
- package/plugins/litclaude/skills/debugging/references/tools/playwright-cli.md +194 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwndbg.md +263 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwntools.md +265 -0
- package/plugins/litclaude/skills/deep-interview/SKILL.md +323 -0
- package/plugins/litclaude/skills/deep-interview/scripts/render_progress.py +193 -0
- package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +62 -0
- package/plugins/litclaude/skills/lit-loop/SKILL.md +144 -0
- package/plugins/litclaude/skills/lit-plan/SKILL.md +125 -0
- package/plugins/litclaude/skills/litgoal/SKILL.md +219 -0
- package/plugins/litclaude/skills/lsp/SKILL.md +63 -0
- package/plugins/litclaude/skills/programming/SKILL.md +106 -0
- package/plugins/litclaude/skills/programming/references/go/README.md +90 -0
- package/plugins/litclaude/skills/programming/references/go/backend-stack.md +641 -0
- package/plugins/litclaude/skills/programming/references/go/bootstrap.md +328 -0
- package/plugins/litclaude/skills/programming/references/go/bubbletea-v2.md +360 -0
- package/plugins/litclaude/skills/programming/references/go/cobra-stack.md +468 -0
- package/plugins/litclaude/skills/programming/references/go/concurrency.md +362 -0
- package/plugins/litclaude/skills/programming/references/go/data-modeling.md +329 -0
- package/plugins/litclaude/skills/programming/references/go/error-handling.md +359 -0
- package/plugins/litclaude/skills/programming/references/go/golangci-strict.md +236 -0
- package/plugins/litclaude/skills/programming/references/go/grpc-connect.md +375 -0
- package/plugins/litclaude/skills/programming/references/go/libraries.md +337 -0
- package/plugins/litclaude/skills/programming/references/go/one-liners.md +202 -0
- package/plugins/litclaude/skills/programming/references/go/sqlc-pgx.md +471 -0
- package/plugins/litclaude/skills/programming/references/go/testing.md +467 -0
- package/plugins/litclaude/skills/programming/references/go/type-patterns.md +298 -0
- package/plugins/litclaude/skills/programming/references/python/README.md +314 -0
- package/plugins/litclaude/skills/programming/references/python/async-anyio.md +442 -0
- package/plugins/litclaude/skills/programming/references/python/data-modeling.md +233 -0
- package/plugins/litclaude/skills/programming/references/python/data-processing.md +133 -0
- package/plugins/litclaude/skills/programming/references/python/error-handling.md +218 -0
- package/plugins/litclaude/skills/programming/references/python/fastapi-stack.md +316 -0
- package/plugins/litclaude/skills/programming/references/python/httpx2-optimization.md +360 -0
- package/plugins/litclaude/skills/programming/references/python/libraries.md +307 -0
- package/plugins/litclaude/skills/programming/references/python/one-liners.md +268 -0
- package/plugins/litclaude/skills/programming/references/python/orjson-stack.md +378 -0
- package/plugins/litclaude/skills/programming/references/python/pydantic-ai.md +285 -0
- package/plugins/litclaude/skills/programming/references/python/pyproject-strict.md +232 -0
- package/plugins/litclaude/skills/programming/references/python/textual-tui.md +201 -0
- package/plugins/litclaude/skills/programming/references/python/type-patterns.md +176 -0
- package/plugins/litclaude/skills/programming/references/rust/README.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/async-tokio.md +299 -0
- package/plugins/litclaude/skills/programming/references/rust/axum-stack.md +467 -0
- package/plugins/litclaude/skills/programming/references/rust/cargo-strict.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/clap-stack.md +409 -0
- package/plugins/litclaude/skills/programming/references/rust/concurrency.md +375 -0
- package/plugins/litclaude/skills/programming/references/rust/libraries.md +439 -0
- package/plugins/litclaude/skills/programming/references/rust/one-liners.md +291 -0
- package/plugins/litclaude/skills/programming/references/rust/proptest-insta.md +429 -0
- package/plugins/litclaude/skills/programming/references/rust/type-state.md +354 -0
- package/plugins/litclaude/skills/programming/references/rust/unsafe-discipline.md +250 -0
- package/plugins/litclaude/skills/programming/references/rust/zero-cost-safety.md +527 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/README.md +289 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
- package/plugins/litclaude/skills/programming/references/typescript/README.md +195 -0
- package/plugins/litclaude/skills/programming/references/typescript/backend-hono.md +672 -0
- package/plugins/litclaude/skills/programming/references/typescript/bootstrap.md +199 -0
- package/plugins/litclaude/skills/programming/references/typescript/data-modeling.md +202 -0
- package/plugins/litclaude/skills/programming/references/typescript/error-handling.md +169 -0
- package/plugins/litclaude/skills/programming/references/typescript/tsconfig-strict.md +152 -0
- package/plugins/litclaude/skills/programming/references/typescript/type-patterns.md +196 -0
- package/plugins/litclaude/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
- package/plugins/litclaude/skills/programming/scripts/go/new-project.py +138 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.editorconfig +13 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.golangci.yml +95 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/ci.yml +37 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/config.go +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/gitignore +15 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/run.go +15 -0
- package/plugins/litclaude/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-project.py +172 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-script.py +116 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
- package/plugins/litclaude/skills/programming/scripts/rust/new-project.py +175 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/new-project.ts +177 -0
- package/plugins/litclaude/skills/refactor/SKILL.md +73 -0
- package/plugins/litclaude/skills/remove-ai-slops/SKILL.md +52 -0
- package/plugins/litclaude/skills/review-work/SKILL.md +331 -0
- package/plugins/litclaude/skills/rules/SKILL.md +66 -0
- package/plugins/litclaude/skills/start-work/SKILL.md +132 -0
- package/scripts/audit-plan-checkboxes.mjs +37 -0
- package/scripts/doctor.mjs +41 -0
- package/scripts/inspect-agent-tools.mjs +27 -0
- package/scripts/postinstall.mjs +50 -0
- package/scripts/qa-claude-plugin-smoke.sh +60 -0
- package/scripts/qa-portable-install.sh +136 -0
- package/scripts/validate-plugin.mjs +72 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Bootstrap — Runtime, Package Manager, Tooling
|
|
2
|
+
|
|
3
|
+
When starting a new TypeScript project (or scripting against the world), the choice of runtime, package manager, framework, and toolchain compounds. The wrong default at minute zero costs hours every week. The right defaults for 2026:
|
|
4
|
+
|
|
5
|
+
## Runtime decision tree
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Is this a CLI / script / single-binary tool?
|
|
9
|
+
└─ Yes → Bun (single executable, hot reload, native TS)
|
|
10
|
+
Use `bun run script.ts` directly. No build step.
|
|
11
|
+
|
|
12
|
+
Is this a backend service?
|
|
13
|
+
├─ Edge (Cloudflare Workers / Vercel / Deno Deploy) → match the platform
|
|
14
|
+
├─ Bun-supported runtime → Bun + Hono
|
|
15
|
+
├─ Need Node-only deps (sharp, native modules without Bun support) → Node + Hono
|
|
16
|
+
└─ Otherwise → Bun + Hono
|
|
17
|
+
|
|
18
|
+
Is this a frontend?
|
|
19
|
+
└─ Vite (regardless of framework). Bun for the package manager.
|
|
20
|
+
|
|
21
|
+
Is this a library to publish to npm?
|
|
22
|
+
└─ tsdown (or unbuild). Targets Node 20+. Use pnpm for monorepo workspaces.
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Bun is the default runtime
|
|
26
|
+
|
|
27
|
+
Use Bun for:
|
|
28
|
+
- Scripts and CLIs (`bun run` is faster than `tsx` and `ts-node`)
|
|
29
|
+
- New backends (Hono runs natively, hot reload via `bun --hot`)
|
|
30
|
+
- Test runner (`bun test` is built-in, faster than vitest for small suites)
|
|
31
|
+
- Package manager (`bun install` is faster than `pnpm` and far faster than `npm`)
|
|
32
|
+
|
|
33
|
+
Use Node when:
|
|
34
|
+
- A dependency uses native modules Bun can't load (rare in 2026; check the dep's release notes)
|
|
35
|
+
- Production target is a Node-specific platform (some serverless platforms don't run Bun yet)
|
|
36
|
+
- You're contributing to a Node-only project
|
|
37
|
+
|
|
38
|
+
`bunx` replaces `npx`. `bun create` scaffolds projects.
|
|
39
|
+
|
|
40
|
+
## Package manager — pnpm > npm
|
|
41
|
+
|
|
42
|
+
If you must use Node, use pnpm. NEVER npm except in legacy projects you don't control.
|
|
43
|
+
|
|
44
|
+
Why pnpm:
|
|
45
|
+
- Content-addressable store: 10x less disk usage on a machine with many projects
|
|
46
|
+
- Strict node_modules layout: phantom dependencies fail at install time, not at runtime
|
|
47
|
+
- Workspaces are first-class
|
|
48
|
+
- Significantly faster than npm
|
|
49
|
+
|
|
50
|
+
Why not yarn:
|
|
51
|
+
- Yarn classic is unmaintained
|
|
52
|
+
- Yarn berry's "PnP" mode breaks with editor tooling more often than it should
|
|
53
|
+
- pnpm has caught up on every yarn berry feature people actually use
|
|
54
|
+
|
|
55
|
+
Why not npm:
|
|
56
|
+
- Slowest of the three
|
|
57
|
+
- No proper workspace story until very recently
|
|
58
|
+
- Phantom dependencies allowed by default
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Convert npm/yarn → pnpm
|
|
62
|
+
pnpm import # reads package-lock.json or yarn.lock and produces pnpm-lock.yaml
|
|
63
|
+
rm -rf node_modules package-lock.json yarn.lock
|
|
64
|
+
pnpm install
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Backend framework — Hono
|
|
68
|
+
|
|
69
|
+
Use Hono for any new HTTP service. It is:
|
|
70
|
+
- Type-safe end-to-end (request/response types flow through middleware)
|
|
71
|
+
- Edge-compatible (runs on Bun, Node, Cloudflare Workers, Deno, AWS Lambda)
|
|
72
|
+
- Faster than Express, Fastify, and most of its peers in synthetic benchmarks
|
|
73
|
+
- Maintained, opinionated, and documented well
|
|
74
|
+
|
|
75
|
+
When Hono → ALWAYS pair with `hono-openapi` + `@scalar/hono-api-reference` + `@hono/swagger-ui`. Full setup with copy-pasteable `app.ts`: [backend-hono.md](backend-hono.md).
|
|
76
|
+
|
|
77
|
+
NEVER:
|
|
78
|
+
- Express for new services. Express is the COBOL of Node — works, but writes itself out of every benchmark.
|
|
79
|
+
- Fastify for new services. Hono ships with better TypeScript ergonomics.
|
|
80
|
+
- NestJS for new services. The Angular-flavoured DI/decorator stack is overkill for ~95% of services.
|
|
81
|
+
- Bare `Bun.serve` or `node:http` unless you have a specific reason. Lose middleware, routing, validation. Reinvent everything.
|
|
82
|
+
|
|
83
|
+
## Frontend tooling — Vite
|
|
84
|
+
|
|
85
|
+
Vite for any frontend. Replaces webpack, parcel, rollup-as-app-bundler. Works with React, Vue, Svelte, Solid, Preact, vanilla.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
bun create vite my-app -- --template react-ts
|
|
89
|
+
cd my-app
|
|
90
|
+
bun install
|
|
91
|
+
bun run dev
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Lint + format — Biome
|
|
95
|
+
|
|
96
|
+
Biome replaces ESLint + Prettier with one tool, written in Rust, ~30x faster.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
bun add --dev @biomejs/biome
|
|
100
|
+
bun biome init
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`biome.json`:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
|
|
108
|
+
"organizeImports": { "enabled": true },
|
|
109
|
+
"linter": { "enabled": true, "rules": { "recommended": true } },
|
|
110
|
+
"formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2 }
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Use ESLint only when:
|
|
115
|
+
- You have an ESLint plugin Biome doesn't replicate (rare in 2026)
|
|
116
|
+
- You're contributing to an existing ESLint project
|
|
117
|
+
|
|
118
|
+
Never run both — pick one.
|
|
119
|
+
|
|
120
|
+
## Test runner — bun test or vitest
|
|
121
|
+
|
|
122
|
+
| Runner | Use when |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `bun test` | Bun project, simple unit tests, no TypeScript path aliases that need vite-style resolution |
|
|
125
|
+
| `vitest` | Vite-based frontend, complex test infrastructure (DOM testing, snapshot, in-browser tests), or you need vitest-specific features |
|
|
126
|
+
|
|
127
|
+
NEVER Jest for a new project. Jest's CommonJS-first design fights every modern Node/TS project.
|
|
128
|
+
|
|
129
|
+
## TypeScript
|
|
130
|
+
|
|
131
|
+
`tsconfig.json` for a Bun + Hono backend:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"compilerOptions": {
|
|
136
|
+
"target": "ESNext",
|
|
137
|
+
"module": "ESNext",
|
|
138
|
+
"moduleResolution": "bundler",
|
|
139
|
+
"lib": ["ESNext"],
|
|
140
|
+
"types": ["bun-types"],
|
|
141
|
+
"strict": true,
|
|
142
|
+
"noUncheckedIndexedAccess": true,
|
|
143
|
+
"noUnusedLocals": true,
|
|
144
|
+
"noUnusedParameters": true,
|
|
145
|
+
"noImplicitReturns": true,
|
|
146
|
+
"noFallthroughCasesInSwitch": true,
|
|
147
|
+
"exactOptionalPropertyTypes": true,
|
|
148
|
+
"esModuleInterop": true,
|
|
149
|
+
"isolatedModules": true,
|
|
150
|
+
"resolveJsonModule": true,
|
|
151
|
+
"skipLibCheck": true,
|
|
152
|
+
"verbatimModuleSyntax": true,
|
|
153
|
+
"noEmit": true
|
|
154
|
+
},
|
|
155
|
+
"include": ["src/**/*", "tests/**/*"]
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`verbatimModuleSyntax: true` enforces explicit `import type { ... }` for type-only imports — pairs with the no-excuse rule on type-only imports.
|
|
160
|
+
|
|
161
|
+
`noEmit: true` because `bun run` and `bun build` handle compilation. The `tsc` command becomes a typechecker only.
|
|
162
|
+
|
|
163
|
+
## Quick-start: Bun + Hono backend
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
mkdir my-api && cd my-api
|
|
167
|
+
bun init -y
|
|
168
|
+
bun add hono hono-openapi @scalar/hono-api-reference @hono/swagger-ui zod
|
|
169
|
+
bun add --dev @biomejs/biome typescript
|
|
170
|
+
bun biome init
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`package.json` scripts:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"scripts": {
|
|
178
|
+
"dev": "bun run --hot src/index.ts",
|
|
179
|
+
"start": "bun run src/index.ts",
|
|
180
|
+
"build": "bun build src/index.ts --target bun --outdir dist",
|
|
181
|
+
"typecheck": "tsc --noEmit",
|
|
182
|
+
"lint": "biome check --write src tests",
|
|
183
|
+
"test": "bun test"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Wire the `app.ts` from [backend-hono.md](backend-hono.md). You have a documented, validated, OpenAPI-spec-emitting service in ~15 minutes.
|
|
189
|
+
|
|
190
|
+
## When NOT to bootstrap from scratch
|
|
191
|
+
|
|
192
|
+
| Situation | Use |
|
|
193
|
+
|---|---|
|
|
194
|
+
| Internal tool with auth/admin/dashboards | Next.js (full-stack) - lots of free wiring |
|
|
195
|
+
| Documentation site | Astro or VitePress |
|
|
196
|
+
| Real-time features (WebRTC, complex sockets) | Bun + Hono + a real-time library |
|
|
197
|
+
| Data-heavy SPA | Vite + React + TanStack Query + TanStack Router |
|
|
198
|
+
|
|
199
|
+
For greenfield backend services, Bun + Hono. Always.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Data Modeling
|
|
2
|
+
|
|
3
|
+
Which construct to use, how to structure data, and why readonly is the default.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Decision flowchart
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Is it a fixed set of named constants?
|
|
11
|
+
YES → as const object + literal union type
|
|
12
|
+
NO ↓
|
|
13
|
+
Is it just branding a primitive (string, number)?
|
|
14
|
+
YES → Branded type
|
|
15
|
+
NO ↓
|
|
16
|
+
Is it an interface / contract?
|
|
17
|
+
YES → interface (structural typing is the default in TS)
|
|
18
|
+
NO ↓
|
|
19
|
+
Does the data cross a trust boundary (user input, API, file)?
|
|
20
|
+
YES → Zod schema + z.infer<typeof schema>
|
|
21
|
+
NO ↓
|
|
22
|
+
Is it a union of possible outcomes?
|
|
23
|
+
YES → Discriminated union (kind/type field)
|
|
24
|
+
NO ↓
|
|
25
|
+
Is it structured data with named fields?
|
|
26
|
+
YES → type alias with readonly properties
|
|
27
|
+
NO → you probably don't need a new type
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Container reference
|
|
33
|
+
|
|
34
|
+
### type alias — internal data
|
|
35
|
+
|
|
36
|
+
The default for structured data inside your codebase. Zero runtime cost.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
type User = {
|
|
40
|
+
readonly id: UserId
|
|
41
|
+
readonly name: string
|
|
42
|
+
readonly email: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type Point = {
|
|
46
|
+
readonly x: number
|
|
47
|
+
readonly y: number
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
All properties `readonly`. Mutable only when mutation is the documented purpose.
|
|
52
|
+
|
|
53
|
+
### interface — contracts and extension
|
|
54
|
+
|
|
55
|
+
Use when you need declaration merging or `extends`.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
interface Repository<T> {
|
|
59
|
+
get(id: string): Promise<T | null>
|
|
60
|
+
save(entity: T): Promise<void>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface UserRepository extends Repository<User> {
|
|
64
|
+
findByEmail(email: string): Promise<User | null>
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### interface vs type — when to use which
|
|
69
|
+
|
|
70
|
+
| Use | When |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `type` | Union types, intersections, mapped types, utility types, internal data shapes |
|
|
73
|
+
| `interface` | Contracts that will be `implements`ed or `extends`ed, declaration merging needed |
|
|
74
|
+
| **Default** | **`type` — unless you have a specific reason for `interface`** |
|
|
75
|
+
|
|
76
|
+
### Zod schema — trust boundary guardian
|
|
77
|
+
|
|
78
|
+
Use when data enters your system. Validates at runtime, infers types at compile time.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { z } from "zod"
|
|
82
|
+
|
|
83
|
+
const CreateUserSchema = z.object({
|
|
84
|
+
name: z.string().min(1),
|
|
85
|
+
email: z.string().email(),
|
|
86
|
+
age: z.number().int().min(0),
|
|
87
|
+
})
|
|
88
|
+
type CreateUser = z.infer<typeof CreateUserSchema>
|
|
89
|
+
|
|
90
|
+
const UserResponseSchema = z.object({
|
|
91
|
+
id: z.string().uuid(),
|
|
92
|
+
name: z.string(),
|
|
93
|
+
email: z.string(),
|
|
94
|
+
})
|
|
95
|
+
type UserResponse = z.infer<typeof UserResponseSchema>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**The one rule**: data crosses a trust boundary → Zod. Everything else → plain type/interface.
|
|
99
|
+
Never use Zod for internal-only data. The runtime validation cost and Zod coupling are unnecessary.
|
|
100
|
+
|
|
101
|
+
### as const — fixed constants
|
|
102
|
+
|
|
103
|
+
Replaces `enum` entirely. Type-safe, tree-shakeable, no runtime overhead.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const ROLES = ["admin", "user", "guest"] as const
|
|
107
|
+
type Role = (typeof ROLES)[number]
|
|
108
|
+
|
|
109
|
+
const STATUS = {
|
|
110
|
+
ACTIVE: "active",
|
|
111
|
+
INACTIVE: "inactive",
|
|
112
|
+
DELETED: "deleted",
|
|
113
|
+
} as const
|
|
114
|
+
type Status = (typeof STATUS)[keyof typeof STATUS]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Discriminated union — multiple outcomes
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
type GetUserResult =
|
|
121
|
+
| { readonly kind: "found"; readonly user: User }
|
|
122
|
+
| { readonly kind: "not_found"; readonly id: UserId }
|
|
123
|
+
| { readonly kind: "forbidden"; readonly reason: string }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Each variant has a `kind` discriminant. TypeScript narrows on `switch (result.kind)`.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Quick lookup
|
|
131
|
+
|
|
132
|
+
| Situation | Use |
|
|
133
|
+
|---|---|
|
|
134
|
+
| User input, API request/response | Zod schema + `z.infer` |
|
|
135
|
+
| Internal value object | `type` with `readonly` properties |
|
|
136
|
+
| Function with multiple outcomes | Discriminated union |
|
|
137
|
+
| Contract for implementations | `interface` |
|
|
138
|
+
| Fixed constants | `as const` + literal union |
|
|
139
|
+
| Distinct primitive (UserId vs OrderId) | Branded type |
|
|
140
|
+
| Dict shape / key-value map | `Record<K, V>` or index signature |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Readonly by default
|
|
145
|
+
|
|
146
|
+
Every property is `readonly` unless mutation is the documented purpose.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// DEFAULT — readonly
|
|
150
|
+
type Config = {
|
|
151
|
+
readonly apiUrl: string
|
|
152
|
+
readonly timeout: number
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Arrays too
|
|
156
|
+
function getUsers(): readonly User[] { ... }
|
|
157
|
+
|
|
158
|
+
// Utility for existing types
|
|
159
|
+
type ReadonlyUser = Readonly<User>
|
|
160
|
+
type DeepReadonlyConfig = Readonly<Config>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
For mutable state (rare), document why:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
/** Counter state — mutation is the entire purpose. */
|
|
167
|
+
type CounterState = {
|
|
168
|
+
count: number // intentionally mutable
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Parse, don't validate
|
|
175
|
+
|
|
176
|
+
Validate at the boundary. Inside the boundary, types are proof of validity.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// BAD — validate then pass raw data
|
|
180
|
+
function processEmail(email: string): void {
|
|
181
|
+
if (!email.includes("@")) throw new Error("invalid")
|
|
182
|
+
// still a raw string downstream
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// GOOD — parse into typed value at boundary
|
|
186
|
+
const EmailSchema = z.string().email().brand("Email")
|
|
187
|
+
type Email = z.infer<typeof EmailSchema>
|
|
188
|
+
|
|
189
|
+
function sendWelcome(email: Email): void { ... }
|
|
190
|
+
|
|
191
|
+
// Boundary code
|
|
192
|
+
const parsed = EmailSchema.parse(rawInput) // Email or throws
|
|
193
|
+
sendWelcome(parsed) // no re-validation needed
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Sources
|
|
199
|
+
|
|
200
|
+
- TypeScript Handbook: [Object Types](https://www.typescriptlang.org/docs/handbook/2/objects.html)
|
|
201
|
+
- Zod: [docs](https://zod.dev)
|
|
202
|
+
- Total TypeScript: [Type vs Interface](https://www.totaltypescript.com/type-vs-interface-which-should-you-use)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Error Handling
|
|
2
|
+
|
|
3
|
+
Typed errors, exhaustive matching, Result pattern, and resource safety.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Typed errors — no bare strings
|
|
8
|
+
|
|
9
|
+
Error classes carry structured data. Callers know exactly what can go wrong.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
class UserNotFoundError extends Error {
|
|
13
|
+
readonly name = "UserNotFoundError"
|
|
14
|
+
constructor(readonly userId: UserId) {
|
|
15
|
+
super(`user ${userId} not found`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class PermissionDeniedError extends Error {
|
|
20
|
+
readonly name = "PermissionDeniedError"
|
|
21
|
+
constructor(
|
|
22
|
+
readonly userId: UserId,
|
|
23
|
+
readonly requiredRole: string,
|
|
24
|
+
) {
|
|
25
|
+
super(`user ${userId} needs role ${requiredRole}`)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// BAD
|
|
32
|
+
throw new Error("user not found")
|
|
33
|
+
throw new Error("permission denied")
|
|
34
|
+
|
|
35
|
+
// GOOD
|
|
36
|
+
throw new UserNotFoundError(userId)
|
|
37
|
+
throw new PermissionDeniedError(userId, "admin")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Always set `readonly name` explicitly — `instanceof` checks survive minification, but `error.name` is more reliable for logging and serialization.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Result pattern — expected failures without exceptions
|
|
45
|
+
|
|
46
|
+
For failures that are **expected** (not found, validation), return a discriminated union instead of throwing.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
type Result<T, E = Error> =
|
|
50
|
+
| { readonly ok: true; readonly value: T }
|
|
51
|
+
| { readonly ok: false; readonly error: E }
|
|
52
|
+
|
|
53
|
+
function ok<T>(value: T): Result<T, never> {
|
|
54
|
+
return { ok: true, value }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function err<E>(error: E): Result<never, E> {
|
|
58
|
+
return { ok: false, error }
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Usage
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
type UserError =
|
|
66
|
+
| { readonly kind: "not_found"; readonly id: UserId }
|
|
67
|
+
| { readonly kind: "forbidden"; readonly reason: string }
|
|
68
|
+
|
|
69
|
+
function getUser(id: UserId): Result<User, UserError> {
|
|
70
|
+
const user = db.find(id)
|
|
71
|
+
if (!user) return err({ kind: "not_found", id })
|
|
72
|
+
if (!user.active) return err({ kind: "forbidden", reason: "deactivated" })
|
|
73
|
+
return ok(user)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Caller must handle both cases
|
|
77
|
+
const result = getUser(userId)
|
|
78
|
+
if (!result.ok) {
|
|
79
|
+
switch (result.error.kind) {
|
|
80
|
+
case "not_found":
|
|
81
|
+
log.warn(`missing: ${result.error.id}`)
|
|
82
|
+
break
|
|
83
|
+
case "forbidden":
|
|
84
|
+
log.error(`denied: ${result.error.reason}`)
|
|
85
|
+
break
|
|
86
|
+
default:
|
|
87
|
+
assertNever(result.error)
|
|
88
|
+
}
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
const user = result.value // narrowed to User
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### When to use which
|
|
95
|
+
|
|
96
|
+
**The heuristic**: caller is 1-2 levels away and MUST handle it → Result. Error should propagate up many layers → throw.
|
|
97
|
+
|
|
98
|
+
| Scenario | Pattern | Why |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| Repository → service (caller handles it) | Result | Caller is right there, must handle both |
|
|
101
|
+
| Validation at boundary (parsing input) | throw (Zod throws) | Propagates up to HTTP handler |
|
|
102
|
+
| Infrastructure failure (network, OOM) | throw | Can't handle locally |
|
|
103
|
+
| Service → service (deep internal) | throw (typed Error subclass) | Result boilerplate across many layers is worse |
|
|
104
|
+
| HTTP handler → response | Catch errors, convert to response | Boundary code catches and translates |
|
|
105
|
+
|
|
106
|
+
**Practical tradeoff**: Result is safest (compiler forces handling) but creates boilerplate when every caller in a chain must check `.ok`. If the error would just propagate through 3+ layers unchanged, use a typed Error subclass instead.
|
|
107
|
+
|
|
108
|
+
### Library or roll your own?
|
|
109
|
+
|
|
110
|
+
Roll your own with the `Result`, `ok`, `err` above. It's 10 lines. Libraries like `neverthrow` add chaining (`.map`, `.andThen`) — use them only if you actually chain results frequently.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Error cause — chain context
|
|
115
|
+
|
|
116
|
+
Use the `cause` option to chain errors without losing the original stack.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
try {
|
|
120
|
+
await db.query(sql)
|
|
121
|
+
} catch (error) {
|
|
122
|
+
throw new DatabaseError("query failed", { cause: error })
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The `cause` is available on `error.cause` and shows up in stack traces.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Exhaustive error handling at boundaries
|
|
131
|
+
|
|
132
|
+
HTTP handlers catch and translate:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
app.onError((error, c) => {
|
|
136
|
+
if (error instanceof UserNotFoundError) {
|
|
137
|
+
return c.json({ error: error.message }, 404)
|
|
138
|
+
}
|
|
139
|
+
if (error instanceof PermissionDeniedError) {
|
|
140
|
+
return c.json({ error: error.message }, 403)
|
|
141
|
+
}
|
|
142
|
+
console.error("unhandled:", error)
|
|
143
|
+
return c.json({ error: "internal server error" }, 500)
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Async error patterns
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// Promise.allSettled — when partial failure is OK
|
|
153
|
+
const results = await Promise.allSettled(urls.map(fetch))
|
|
154
|
+
const successes = results
|
|
155
|
+
.filter((r): r is PromiseFulfilledResult<Response> => r.status === "fulfilled")
|
|
156
|
+
.map((r) => r.value)
|
|
157
|
+
|
|
158
|
+
// AbortSignal — cancellation
|
|
159
|
+
async function fetchWithTimeout(url: string, ms: number): Promise<Response> {
|
|
160
|
+
return fetch(url, { signal: AbortSignal.timeout(ms) })
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Sources
|
|
167
|
+
|
|
168
|
+
- MDN: [Error cause](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause)
|
|
169
|
+
- MDN: [Promise.allSettled](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled)
|