diff --git a/packages/omo-codex/plugin/skills/programming/references/typescript/README.md b/packages/omo-codex/plugin/skills/programming/references/typescript/README.md new file mode 100644 index 000000000..f7c9bd1e0 --- /dev/null +++ b/packages/omo-codex/plugin/skills/programming/references/typescript/README.md @@ -0,0 +1,195 @@ + +# TypeScript Programmer + +Modern TypeScript. Type-strict, stack-first, async-correct. + +## Philosophy + +The compiler is your proof system. Make illegal states unrepresentable. Parse at boundaries. Every function has a contract; the type system enforces it. + +## Hard rules + +These are deliberate project choices. Violations are always wrong, not "style preferences". + +### Tooling + +| Category | Use | Never | +|---|---|---| +| Runtime | Bun (native TS, single binary) | ts-node, tsx | +| Package manager | `pnpm` | npm, yarn (unless workspace requires it) | +| Linter + formatter | Biome | ESLint, Prettier | +| Type checker | `tsc --noEmit` with strict config | skip type checking | +| Web framework | Hono | Express | +| Validation | Zod | joi, yup, class-validator | +| Testing | `bun test` or vitest | jest | +| ORM | Drizzle | TypeORM, Prisma (unless already in project) | + +### The iron list + +1. **Readonly by default** — all `type`/`interface` properties are `readonly`. Arrays are `readonly T[]`. Mutable only when mutation is the documented purpose. +2. **Branded types for distinct IDs** — `type UserId = Brand`. Never pass raw `string` where a branded type exists. +3. **Exhaustive switch** — every `switch` on a discriminated union ends with `default: assertNever(x)`. No fall-through. +4. **No any** — `any` is banned in annotations, returns, and parameters. Use `unknown` and narrow. +5. **No type assertions** — `as any`, `as unknown` banned. `as const` and `satisfies` are fine. +6. **No non-null assertion** — `x!` is banned. Use narrowing or optional chaining (`x?.y`). +7. **No @ts-ignore / @ts-expect-error** — fix the type. +8. **No enum** — use `as const` objects + literal union types. +9. **Zod at boundaries** — external input (API, user, file) → Zod schema. Internal → plain types. +10. **Typed errors** — Error subclasses with typed fields. No `throw new Error("bare string")` for domain errors. Use Result for expected failures within 1-2 call levels; throw for propagation across many layers. +11. **as const for constants** — module-level constant objects and arrays use `as const`. +12. **import type** — type-only imports use `import type`. Enforced by `verbatimModuleSyntax`. +13. **Named exports only** — no `export default`. Exception: framework requirement (Next.js pages, etc.). +14. **No empty catch, no catch-and-swallow** — every `catch` block must either (a) narrow the error with `instanceof` and handle each case, or (b) re-throw. Empty catch blocks and `catch (e) { console.error(e) }` without narrowing or re-throw are banned — they hide bugs. At top-level boundaries (CLI entry, HTTP handler), opt out with `// no-excuse-ok: catch`. + +### Data modeling — which construct, when + +| Situation | Use | +|---|---| +| User input, API request/response | Zod schema + `z.infer` | +| Internal value object | `type` with `readonly` properties | +| Function with multiple outcomes | Discriminated union (`kind` field) | +| Contract for implementations | `interface` | +| Fixed constants | `as const` + literal union | +| Distinct primitive (UserId vs OrderId) | Branded type | +| Key-value map | `Record` or index signature | + +**The one rule**: data crosses trust boundary → Zod. Everything else → plain `type` with `readonly`. + +Load `data-modeling.md` for the full decision flowchart and comparison. + +### When readonly does not apply + +- **Framework state** (React `useState`, signals) — managed by framework. +- **Builder / accumulator** — object exists to be mutated (buffer, cache). Document why. +- **ORM mutations** — Drizzle insert/update objects. + +### Why empty/unhandled catch is banned + +In TypeScript, every `catch` receives `unknown`. The language gives you no type safety in catch blocks — you must earn it with `instanceof`. A bare `catch (e) { console.error(e) }` swallows `TypeError`, `RangeError`, and your domain errors identically. When a new error type appears, nothing warns you. + +```typescript +// BANNED — empty catch +try { await fetchData() } catch {} +try { await fetchData() } catch (e) { /* will fix later */ } + +// BANNED — catch-and-swallow (no narrowing, no rethrow) +try { + const data = await api.get("/users") +} catch (e) { + console.error("failed", e) +} + +// GOOD — narrow with instanceof +try { + const data = await api.get("/users") +} catch (e) { + if (e instanceof HttpError) { + logger.warn(`API ${e.status}: ${e.message}`) + return fallback + } + throw e // unknown errors propagate +} + +// GOOD — top-level boundary (only place catch-all is acceptable) +async function main(): Promise { // no-excuse-ok: catch + try { + await run() + } catch (e) { + console.error("unhandled:", e) + process.exit(1) + } +} +``` + +### Libraries + +| Domain | Library | Why | +|---|---|---| +| HTTP framework | Hono | Lightweight, multi-runtime, middleware, OpenAPI | +| Validation | Zod | Runtime validation + type inference | +| ORM | Drizzle | Type-safe SQL, no codegen | +| HTTP client | `ky` | Thin fetch wrapper (5KB); auto-throw on non-2xx, retry, timeout, hooks, prefixUrl. Browser + Node + Bun + Deno | +| HTTP client (perf) | `undici` (direct API) | When a Node backend needs connection pooling, HTTP/2, or pipelining | + +> **HTTP client rule** - production code must not use bare `fetch()`. It has no retry, timeout, or error-handling policy and causes silent failures during incidents. Install **`ky`** by default, and use the **`undici`** direct API when a Node backend needs high-volume requests, connection pooling, HTTP/2, or pipelining. ~~`axios`~~ is forbidden after the supply-chain compromise (2026-03). `node-fetch` is unnecessary because Node 18+ includes built-in fetch. +| Testing | `bun test` / vitest | Fast, ESM-native | +| Logging | `pino` | Structured JSON, fast | +| CLI | `@clack/prompts` + `commander` | Interactive + parsing | + +## tsconfig — the one true config + +Scaffold a new project with all strict defaults pre-configured: + +```bash +bun run ../../scripts/typescript/new-project.ts my-api +bun run ../../scripts/typescript/new-project.ts my-api --path ./projects +``` + +Creates: `package.json` (Hono + Zod + Biome), `tsconfig.json` (ultra-strict), `biome.json`, `src/index.ts`, `.gitignore`. Works on macOS, Linux, Windows. + +For manual setup: `bunx tsc --init`, then load `tsconfig-strict.md` for the full strict config. + +Key flags beyond `"strict": true`: + +| Flag | What it catches | +|---|---| +| `noUncheckedIndexedAccess` | `arr[0]` is `T \| undefined`, forces check | +| `exactOptionalPropertyTypes` | `{ x?: string }` ≠ `{ x: string \| undefined }` | +| `verbatimModuleSyntax` | Forces `import type` for type-only imports | +| `noFallthroughCasesInSwitch` | Forgotten `break` / `return` | +| `noPropertyAccessFromIndexSignature` | `.key` on index sig → bracket notation | + +## Reference loading + +Load on demand — not all at once. + +| Need | Load | +|---|---| +| Strict tsconfig + Biome config | `tsconfig-strict.md` | +| Type patterns (branded, as const, satisfies, narrowing, assertNever) | `type-patterns.md` | +| Data modeling (type vs interface vs Zod, readonly, parse-don't-validate) | `data-modeling.md` | +| Error handling (Result, typed errors, union vs throw) | `error-handling.md` | +| Bootstrapping a new project (Bun, pnpm, Hono, Vite) | `bootstrap.md` | +| Hono backend stack (hono-openapi, Scalar, Swagger) | `backend-hono.md` | + +## No-excuse audit + +Violations caught by `../../scripts/typescript/check-no-excuse-rules.ts`. Run after every edit session. + +| Rule ID | Catches | Opt-out | +|---|---|---| +| `no-any-assertion` | `as any` | None — redesign types | +| `no-unknown-assertion` | `as unknown` | None — redesign types | +| `no-ts-ignore` | `@ts-ignore` | None — fix the type | +| `no-ts-expect-error` | `@ts-expect-error` | None — fix the type | +| `no-enum` | `enum` declarations | None — use `as const` | +| `no-non-null-assertion` | `x!` postfix | None — narrow or `?.` | +| `no-throw-literal` | `throw "string"` / `throw 123` | None — throw Error subclass | +| `no-mutable-export` | `export let` / `export var` | None — use `export const` | +| `no-any-annotation` | `: any` in parameter/return/variable types | `// no-excuse-ok: any` | +| `no-explicit-any-return` | `(): any` or `(): Promise` return types | `// no-excuse-ok: any` | +| `empty-catch` | `catch { }` or `catch (e) { }` with empty body | `// no-excuse-ok: catch` | +| `catch-without-narrowing` | `catch (e)` used without `instanceof` or re-throw | `// no-excuse-ok: catch` | + +Biome enforces additional rules (noExplicitAny, noNonNullAssertion, noDefaultExport, useImportType). The script catches what Biome cannot. + +## In tests + +Tests are strict too, with these exceptions (configure in `biome.jsonc` overrides): + +| In tests you may | Why | +|---|---| +| Use `expect()` assertions | That's how testing works | +| Use magic numbers | Test data | +| Access private members via bracket notation | Testing internals | +| Skip readonly on test fixtures | Mutable setup/teardown | + +Tests still follow the iron list — branded types, typed errors, exhaustive switch. + +## Existing codebases + +When editing an existing file that doesn't follow these rules: **write new code in strict style, don't refactor existing code in the same change.** + +## Activation + +This skill activates whenever you are writing or modifying any `.ts` or `.tsx` file. Even one-off scripts get the strict treatment. diff --git a/packages/omo-codex/plugin/skills/programming/references/typescript/backend-hono.md b/packages/omo-codex/plugin/skills/programming/references/typescript/backend-hono.md new file mode 100644 index 000000000..821754fdb --- /dev/null +++ b/packages/omo-codex/plugin/skills/programming/references/typescript/backend-hono.md @@ -0,0 +1,672 @@ +# Hono Backend Stack Reference (2026) + +> **Canonical stack**: `hono` + `hono-openapi` + `@scalar/hono-api-reference` + `@hono/swagger-ui` +> **Runtime**: Bun (TypeScript-first) +> **Validator**: Zod v4 (Standard Schema compliant, zero extra deps for OpenAPI) + +--- + +## 1. Package Versions (Latest Stable) + +| Package | Version | Source | +|---------|---------|--------| +| `hono` | `^4.12.5` | [peer dep of scalar](https://github.com/scalar/scalar/blob/8bcf8bf52a0da667d44eeec08648e3b1da044f97/integrations/hono/package.json#L66) | +| `hono-openapi` | `^1.3.0` | [npm](https://registry.npmjs.org/hono-openapi) — published Mar 2, 2026 | +| `@scalar/hono-api-reference` | `^0.10.11` | [npm](https://www.npmjs.com/package/@scalar/hono-api-reference) — published Apr 28, 2026 | +| `@hono/swagger-ui` | `^0.6.1` | [npm](https://www.npmjs.com/package/@hono/swagger-ui) — published Apr 2026 | +| `zod` | `^4.4.1` | [npm registry](https://registry.npmjs.org/zod) — latest stable v4 | + +### `package.json` dependency block + +```json +{ + "dependencies": { + "hono": "^4.12.5", + "hono-openapi": "^1.3.0", + "@scalar/hono-api-reference": "^0.10.11", + "@hono/swagger-ui": "^0.6.1", + "zod": "^4.4.1" + }, + "devDependencies": { + "typescript": "^5.8.0", + "@types/bun": "latest" + } +} +``` + +> **Peer dependencies auto-installed by `hono-openapi`**: +> - `@hono/standard-validator@^0.2.0` +> - `@standard-community/standard-json@^0.3.5` +> - `@standard-community/standard-openapi@^0.2.9` +> - `openapi-types@^12.1.3` +> +> [Source: `hono-openapi/package.json` peerDependencies](https://github.com/rhinobase/hono-openapi/blob/10f45a66ede3764b5e6065805fb60fd5df090466/package.json#L50-L65) + +--- + +## 2. Complete `app.ts` — Copy-Pasteable + +```typescript +import { Hono } from 'hono' +import { describeRoute, openAPIRouteHandler, resolver, validator } from 'hono-openapi' +import { Scalar } from '@scalar/hono-api-reference' +import { swaggerUI } from '@hono/swagger-ui' +import { z } from 'zod' + +// ─────────────────────────────────────────────────────────────── +// 1. Schema definitions (Zod v4 — Standard Schema native) +// ─────────────────────────────────────────────────────────────── + +const QuerySchema = z.object({ + name: z.string().optional(), +}) + +const ResponseSchema = z.object({ + message: z.string(), +}) + +const JsonBodySchema = z.object({ + name: z.string(), + age: z.number().int().min(0), +}) + +// ─────────────────────────────────────────────────────────────── +// 2. Hono app with described routes +// ─────────────────────────────────────────────────────────────── + +const app = new Hono() + +// Health check (no validation) +app.get('/health', (c) => c.json({ status: 'ok' })) + +// A fully-documented route +app.get( + '/hello', + describeRoute({ + tags: ['Greetings'], + summary: 'Say hello', + description: 'Returns a greeting message', + responses: { + 200: { + description: 'Successful greeting', + content: { + 'application/json': { + schema: resolver(ResponseSchema), + }, + }, + }, + }, + }), + validator('query', QuerySchema), + (c) => { + const query = c.req.valid('query') + return c.json({ message: `Hello ${query.name ?? 'Hono'}!` }) + }, +) + +// A POST route with JSON body validation +app.post( + '/users', + describeRoute({ + tags: ['Users'], + summary: 'Create a user', + responses: { + 200: { + description: 'User created', + content: { + 'application/json': { + schema: resolver(ResponseSchema), + }, + }, + }, + }, + }), + validator('json', JsonBodySchema), + (c) => { + const body = c.req.valid('json') + return c.json({ message: `Created user ${body.name}` }) + }, +) + +// ─────────────────────────────────────────────────────────────── +// 3. OpenAPI spec endpoint +// ─────────────────────────────────────────────────────────────── + +app.get( + '/openapi.json', + openAPIRouteHandler(app, { + documentation: { + info: { + title: 'Hono API', + version: '1.0.0', + description: 'Example Hono API with OpenAPI', + }, + servers: [ + { url: 'http://localhost:3000', description: 'Local server' }, + ], + }, + }), +) + +// ─────────────────────────────────────────────────────────────── +// 4. Scalar API Reference UI +// ─────────────────────────────────────────────────────────────── + +app.get( + '/scalar', + Scalar({ + url: '/openapi.json', + theme: 'saturn', + pageTitle: 'Hono API Reference', + }), +) + +// ─────────────────────────────────────────────────────────────── +// 5. Swagger UI (parallel mount) +// ─────────────────────────────────────────────────────────────── + +app.get( + '/swagger', + swaggerUI({ + url: '/openapi.json', + title: 'Swagger UI', + }), +) + +// ─────────────────────────────────────────────────────────────── +// 6. Bun canonical entrypoint +// ─────────────────────────────────────────────────────────────── + +export default app +``` + +--- + +## 3. `hono-openapi` API Reference + +### Import paths + +**There is only one import path.** `hono-openapi` exports everything from its root: + +```typescript +import { + describeRoute, // middleware to annotate a route with OpenAPI metadata + describeResponse, // attach response schemas directly to a handler + validator, // validation middleware (wraps @hono/standard-validator) + resolver, // wrap a Zod/Valibot/etc schema for OpenAPI responses + openAPIRouteHandler, // serve the generated OpenAPI JSON document + generateSpecs, // programmatically generate the spec (for build-time caching) +} from 'hono-openapi' +``` + +**Evidence** ([`src/index.ts`](https://github.com/rhinobase/hono-openapi/blob/10f45a66ede3764b5e6065805fb60fd5df090466/src/index.ts#L1-L9)): + +```typescript +export { generateSpecs, openAPIRouteHandler } from "./handler.js"; +export { + describeResponse, + describeRoute, + loadVendor, + resolver, + validator, +} from "./middlewares.js"; +``` + +> **No subpath exports** such as `hono-openapi/zod` or `hono-openapi/valibot`. The package uses Standard Schema and auto-detects the validator vendor. + +### `describeRoute()` middleware + +Attach OpenAPI metadata to any Hono route. Use `resolver()` for response body schemas. + +```typescript +app.get( + '/path', + describeRoute({ + tags: ['Users'], + summary: 'Get user', + description: 'Retrieve a single user by ID', + responses: { + 200: { + description: 'User found', + content: { + 'application/json': { + schema: resolver(UserSchema), + }, + }, + }, + 404: { + description: 'User not found', + }, + }, + }), + handler, +) +``` + +**Evidence** ([`src/middlewares.ts`](https://github.com/rhinobase/hono-openapi/blob/10f45a66ede3764b5e6065805fb60fd5df090466/src/middlewares.ts#L244-L254)): + +```typescript +export function describeRoute(spec: DescribeRouteOptions): MiddlewareHandler { + const middleware: MiddlewareHandler = async (_c, next) => { + await next(); + }; + return Object.assign(middleware, { + [uniqueSymbol]: { spec }, + }); +} +``` + +### `validator()` middleware + +Validates `query`, `json`, `param`, or `form` and **automatically** injects the request schema into the OpenAPI document. No manual `request` block in `describeRoute()` is required. + +```typescript +validator('query', QuerySchema) // ?name=foo +validator('json', JsonBodySchema) // POST body +validator('param', ParamSchema) // /users/:id +validator('form', FormSchema) // multipart/form-data +``` + +**Evidence** ([`src/middlewares.ts`](https://github.com/rhinobase/hono-openapi/blob/10f45a66ede3764b5e6065805fb60fd5df090466/src/middlewares.ts#L199-L237)): + +```typescript +export function validator( + target: Target, + schema: Schema, + hook?: Hook<...>, + options?: ResolverReturnType["options"], +): MiddlewareHandler { + const middleware = sValidator(target, schema, hook); + return Object.assign(middleware, { + [uniqueSymbol]: { target, ...resolver(schema, options), options }, + }); +} +``` + +### `openAPIRouteHandler()` — serving the spec + +```typescript +app.get( + '/openapi.json', + openAPIRouteHandler(app, { + documentation: { + info: { title: 'Hono API', version: '1.0.0' }, + servers: [{ url: 'http://localhost:3000' }], + }, + }), +) +``` + +**Evidence** ([`src/handler.ts`](https://github.com/rhinobase/hono-openapi/blob/10f45a66ede3764b5e6065805fb60fd5df090466/src/handler.ts#L42-L59)): + +```typescript +export function openAPIRouteHandler<...>( + hono: Hono, + options?: Partial, +): MiddlewareHandler { + let specs: OpenAPIV3_1.Document; + return async (c) => { + if (specs) return c.json(specs); + specs = await generateSpecs(hono, options, c); + return c.json(specs); + }; +} +``` + +> **Mount path convention**: `/openapi.json` is the most common. Some projects use `/openapi/spec.json` (e.g. [NamesMT/starter-monorepo](https://github.com/NamesMT/starter-monorepo/blob/main/apps/backend/src/openAPI.ts)). + +--- + +## 4. `@scalar/hono-api-reference` Setup + +### Import path and package name + +```typescript +import { Scalar } from '@scalar/hono-api-reference' +``` + +> **Deprecated**: `apiReference` is still exported but deprecated in favor of `Scalar` ([PR #5297](https://github.com/scalar/scalar/pull/5297)). + +**Evidence** ([`integrations/hono/src/index.ts`](https://github.com/scalar/scalar/blob/8bcf8bf52a0da667d44eeec08648e3b1da044f97/integrations/hono/src/index.ts#L1-L9)): + +```typescript +import { Scalar } from './scalar' +export { + Scalar, + /** + * @deprecated Use `Scalar` instead. + */ + Scalar as apiReference, +} +``` + +### Mount path convention + +Common choices: +- `/scalar` — matches the middleware name +- `/docs` — generic documentation endpoint +- `/openapi/ui` — nested under the OpenAPI prefix + +### Configuration options + +The Hono middleware accepts the **universal Scalar configuration** plus Hono-specific overrides (`pageTitle`, `cdn`). + +```typescript +app.get('/scalar', Scalar({ + // ── Source (required) ── + url: '/openapi.json', // URL to the OpenAPI spec + + // ── Appearance ── + theme: 'saturn', // 'alternate' | 'default' | 'moon' | 'purple' + // | 'solarized' | 'bluePlanet' | 'deepSpace' + // | 'saturn' | 'kepler' | 'elysiajs' | 'fastify' + // | 'mars' | 'laserwave' | 'none' + pageTitle: 'My API Docs', // HTML + customCss: '.sidebar { ... }', // injected <style> block + metaData: { title: '...' }, // SEO meta tags (unhead format) + favicon: '/favicon.svg', + + // ── Behavior ── + layout: 'modern', // 'modern' | 'classic' + darkMode: true, + forceDarkModeState: 'dark', // 'dark' | 'light' + hideDarkModeToggle: false, + hideModels: false, + hideSearch: false, + hideTestRequestButton: false, + showOperationId: false, + showSidebar: true, + + // ── Proxy / Server ── + proxyUrl: 'https://proxy.scalar.com', + baseServerURL: 'http://localhost:3000', + servers: [{ url: 'http://localhost:3000' }], + + // ── CDN ── + cdn: 'https://cdn.jsdelivr.net/npm/@scalar/api-reference', + + // ── Advanced ── + authentication: { ... }, + hiddenClients: ['unirest'], + defaultHttpClient: { targetKey: 'js', clientKey: 'fetch' }, + plugins: [...], + pathRouting: { basePath: '/reference' }, + mcp: { name: 'My MCP', url: '...' }, +})) +``` + +**Evidence** — Scalar types define the full schema: +- [Base configuration (themes, proxy, etc.)](https://github.com/scalar/scalar/blob/8bcf8bf52a0da667d44eeec08648e3b1da044f97/packages/types/src/api-reference/base-configuration.ts#L110-L129) +- [HTML rendering configuration (`pageTitle`, `cdn`)](https://github.com/scalar/scalar/blob/8bcf8bf52a0da667d44eeec08648e3b1da044f97/packages/types/src/api-reference/html-rendering-configuration.ts#L8-L23) +- [Source configuration (`url`, `content`)](https://github.com/scalar/scalar/blob/8bcf8bf52a0da667d44eeec08648e3b1da044f97/packages/types/src/api-reference/source-configuration.ts#L8-L55) +- [Full API reference configuration](https://github.com/scalar/scalar/blob/8bcf8bf52a0da667d44eeec08648e3b1da044f97/packages/types/src/api-reference/api-reference-configuration.ts#L22-L379) + +### Dynamic configuration (request-aware) + +```typescript +app.get('/scalar', Scalar((c) => ({ + url: '/openapi.json', + proxyUrl: c.env.ENVIRONMENT === 'development' + ? 'https://proxy.scalar.com' + : undefined, +}))) +``` + +**Evidence** ([`integrations/hono/src/scalar.ts`](https://github.com/scalar/scalar/blob/8bcf8bf52a0da667d44eeec08648e3b1da044f97/integrations/hono/src/scalar.ts#L75-L94)): + +```typescript +export const Scalar = <E extends Env>(configOrResolver: Configuration<E>): MiddlewareHandler<E> => { + return async (c) => { + let resolvedConfig: Partial<ApiReferenceConfiguration> = {} + if (typeof configOrResolver === 'function') { + resolvedConfig = await configOrResolver(c) + } else { + resolvedConfig = configOrResolver + } + // ... + } +} +``` + +--- + +## 5. `@hono/swagger-ui` Setup + +### Import path and package name + +```typescript +import { swaggerUI } from '@hono/swagger-ui' +``` + +**Evidence** ([`packages/swagger-ui/src/index.ts`](https://github.com/honojs/middleware/blob/eb443a2fbda674bbe12d3f30e96854bb0cad6232/packages/swagger-ui/src/index.ts#L93)): + +```typescript +export { middleware as swaggerUI, SwaggerUI } +``` + +### Mount path convention + +Common choices: +- `/swagger` — explicit +- `/ui` — used in Hono official examples +- `/docs` — generic + +### Configuration options + +```typescript +app.get('/swagger', swaggerUI({ + url: '/openapi.json', // URL to the OpenAPI spec (required) + title: 'Swagger UI', // HTML page title + version: 'latest', // Swagger UI CDN version + // Any standard Swagger UI option also works: + // presets, plugins, urls, etc. +})) +``` + +**Evidence** ([`packages/swagger-ui/src/index.ts`](https://github.com/honojs/middleware/blob/eb443a2fbda674bbe12d3f30e96854bb0cad6232/packages/swagger-ui/src/index.ts#L8-L43)): + +```typescript +type SwaggerUIOptions = OriginalSwaggerUIOptions & DistSwaggerUIOptions + +const middleware = <E extends Env>(options: SwaggerUIOptions): MiddlewareHandler<E> => + async (c) => { + const title = options?.title ?? 'SwaggerUI' + return c.html(/* html */ `...`) + } +``` + +--- + +## 6. Bun Runtime Entrypoint + +### Canonical shape for `bun run` + +```typescript +import { Hono } from 'hono' + +const app = new Hono() +// ... routes ... + +export default app +``` + +**Evidence** ([Hono Bun docs](https://hono.dev/docs/getting-started/bun)): + +> ```ts +> import { Hono } from 'hono' +> const app = new Hono() +> app.get('/', (c) => c.text('Hello Bun!')) +> export default app +> ``` + +### Custom port + +```typescript +export default { + port: 3000, + fetch: app.fetch, +} +``` + +**Evidence** ([Hono Bun docs — Change port number](https://hono.dev/docs/getting-started/bun)): + +> ```ts +> export default { +> port: 3000, +> fetch: app.fetch, +> } +> ``` + +### `package.json` scripts for Bun + +```json +{ + "scripts": { + "dev": "bun run --hot src/index.ts", + "start": "bun run src/index.ts", + "build": "tsc --noEmit" + } +} +``` + +--- + +## 7. OpenAPI Version + +### `hono-openapi` emits **OpenAPI 3.1.0** by default + +This is **hardcoded** in the source and **not configurable** at runtime: + +**Evidence** ([`src/handler.ts` line 120](https://github.com/rhinobase/hono-openapi/blob/10f45a66ede3764b5e6065805fb60fd5df090466/src/handler.ts#L120)): + +```typescript +return { + openapi: "3.1.0", + ..._documentation, + // ... +} satisfies OpenAPIV3_1.Document; +``` + +> If you need OpenAPI 3.0.x, you must post-process the generated spec or use `@hono/zod-openapi` (the older package) instead. The user explicitly requested `hono-openapi`, so document that 3.1.0 is the only output. + +--- + +## 8. Zod v3 vs Zod v4 + +| Feature | Zod v3 | Zod v4 | +|---------|--------|--------| +| Standard Schema | ❌ No | ✅ Yes (native) | +| `hono-openapi` extra deps | `zod-openapi@4` | None | +| Import path | `import { z } from 'zod'` | `import { z } from 'zod'` (or `zod/v4` for explicit) | + +**For Zod v3 users**, install the compatibility layer: + +```bash +npm install zod-openapi@4 +``` + +Then use `zod-openapi`'s `.openapi()` for metadata and `.meta({ ref: 'Name' })` for component references. `hono-openapi`'s `resolver()` will still work, but the underlying schema conversion relies on `zod-openapi@4`. + +**Evidence** ([HonoHub Zod docs](https://honohub.dev/docs/openapi/zod)): + +> "For zod v3, you can use the `zod-openapi` library. You need to install `zod-openapi@4` for this to work properly." + +**For Zod v4 users** (recommended in 2026), no extra packages are needed. `z.date()` is automatically converted to `{ type: 'string', format: 'date-time' }`. + +**Evidence** ([`src/middlewares.ts` Zod v4 date override](https://github.com/rhinobase/hono-openapi/blob/10f45a66ede3764b5e6065805fb60fd5df090466/src/middlewares.ts#L63-L71)): + +```typescript +const zodV4DateOverride = (ctx: { ... }) => { + if (ctx.zodSchema._zod.def.type === "date") { + ctx.jsonSchema.type = "string"; + ctx.jsonSchema.format = "date-time"; + } +}; +``` + +--- + +## 9. Real-World Example + +**NamesMT/starter-monorepo** — a public monorepo starter using `hono-openapi` + `@scalar/hono-api-reference` together: + +- File: [`apps/backend/src/openAPI.ts`](https://github.com/NamesMT/starter-monorepo/blob/main/apps/backend/src/openAPI.ts) +- Pattern: mounts spec at `/openapi/spec.json` and Scalar UI at `/openapi/ui` + +```typescript +import type { Hono } from 'hono' +import { Scalar } from '@scalar/hono-api-reference' +import { openAPIRouteHandler } from 'hono-openapi' + +export function setupOpenAPI(app: Hono<any, any>, prefix = '/openapi') { + app.get( + `${prefix}/spec.json`, + openAPIRouteHandler(app, { + documentation: { + info: { + title: `starter-monorepo's backend`, + version: '1.0.0', + description: 'My amazing API', + }, + }, + }), + ) + + app.get( + `${prefix}/ui`, + Scalar({ + theme: 'deepSpace', + url: `${prefix}/spec.json`, + }), + ) +} +``` + +> **Note**: No public repo was found using all four (`hono-openapi` + `Scalar` + `swagger-ui` + `hono`) in a single file. The canonical combination in the wild is `hono-openapi` + `Scalar`. Adding `swagger-ui` is a trivial parallel mount (shown in the `app.ts` above). + +--- + +## 10. Common Pitfalls + +1. **Using `openAPISpecs` instead of `openAPIRouteHandler`** + Some docs (e.g. HONC) use `openAPISpecs` — this is **not** the current export name. The correct function is `openAPIRouteHandler` ([source](https://github.com/rhinobase/hono-openapi/blob/10f45a66ede3764b5e6065805fb60fd5df090466/src/index.ts#L1)). + +2. **Importing from `hono-openapi/zod`** + There are **no subpath exports**. Always import from `hono-openapi` directly. + +3. **Forgetting `@hono/standard-validator`** + It is a peer dependency of `hono-openapi`. Modern package managers (npm ≥ 7, pnpm, bun) auto-install it. If you see validation errors, ensure it is present in `node_modules`. + +4. **Using `@hono/zod-openapi` (the OLD package)** + The user explicitly wants `hono-openapi` (the newer, middleware-based, Standard Schema package). Do not confuse with `@hono/zod-openapi` which wraps the `Hono` class into `OpenAPIHono`. + +5. **Swagger UI `spec` option** + `@hono/swagger-ui` does **not** accept a `spec` option to embed the document directly. It only accepts `url` (or `urls`) pointing to an external spec endpoint. If you need embedded specs, use Scalar's `content` option instead. + +--- + +## 11. Quick Start Commands + +```bash +# 1. Create project +mkdir my-api && cd my-api +bun init -y + +# 2. Install dependencies +bun add hono hono-openapi @scalar/hono-api-reference @hono/swagger-ui zod + +# 3. Add TypeScript +bun add -d typescript @types/bun + +# 4. Write app.ts (copy from section 2 above) +# 5. Run +bun run --hot app.ts +``` + +Endpoints after startup: +- `GET /health` — health check +- `GET /hello?name=world` — documented route +- `POST /users` — validated JSON body route +- `GET /openapi.json` — raw OpenAPI 3.1.0 spec +- `GET /scalar` — Scalar API Reference UI +- `GET /swagger` — Swagger UI diff --git a/packages/omo-codex/plugin/skills/programming/references/typescript/bootstrap.md b/packages/omo-codex/plugin/skills/programming/references/typescript/bootstrap.md new file mode 100644 index 000000000..f0981b795 --- /dev/null +++ b/packages/omo-codex/plugin/skills/programming/references/typescript/bootstrap.md @@ -0,0 +1,199 @@ +# Bootstrap — Runtime, Package Manager, Tooling + +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: + +## Runtime decision tree + +``` +Is this a CLI / script / single-binary tool? +└─ Yes → Bun (single executable, hot reload, native TS) + Use `bun run script.ts` directly. No build step. + +Is this a backend service? +├─ Edge (Cloudflare Workers / Vercel / Deno Deploy) → match the platform +├─ Bun-supported runtime → Bun + Hono +├─ Need Node-only deps (sharp, native modules without Bun support) → Node + Hono +└─ Otherwise → Bun + Hono + +Is this a frontend? +└─ Vite (regardless of framework). Bun for the package manager. + +Is this a library to publish to npm? +└─ tsdown (or unbuild). Targets Node 20+. Use pnpm for monorepo workspaces. +``` + +## Bun is the default runtime + +Use Bun for: +- Scripts and CLIs (`bun run` is faster than `tsx` and `ts-node`) +- New backends (Hono runs natively, hot reload via `bun --hot`) +- Test runner (`bun test` is built-in, faster than vitest for small suites) +- Package manager (`bun install` is faster than `pnpm` and far faster than `npm`) + +Use Node when: +- A dependency uses native modules Bun can't load (rare in 2026; check the dep's release notes) +- Production target is a Node-specific platform (some serverless platforms don't run Bun yet) +- You're contributing to a Node-only project + +`bunx` replaces `npx`. `bun create` scaffolds projects. + +## Package manager — pnpm > npm + +If you must use Node, use pnpm. NEVER npm except in legacy projects you don't control. + +Why pnpm: +- Content-addressable store: 10x less disk usage on a machine with many projects +- Strict node_modules layout: phantom dependencies fail at install time, not at runtime +- Workspaces are first-class +- Significantly faster than npm + +Why not yarn: +- Yarn classic is unmaintained +- Yarn berry's "PnP" mode breaks with editor tooling more often than it should +- pnpm has caught up on every yarn berry feature people actually use + +Why not npm: +- Slowest of the three +- No proper workspace story until very recently +- Phantom dependencies allowed by default + +```bash +# Convert npm/yarn → pnpm +pnpm import # reads package-lock.json or yarn.lock and produces pnpm-lock.yaml +rm -rf node_modules package-lock.json yarn.lock +pnpm install +``` + +## Backend framework — Hono + +Use Hono for any new HTTP service. It is: +- Type-safe end-to-end (request/response types flow through middleware) +- Edge-compatible (runs on Bun, Node, Cloudflare Workers, Deno, AWS Lambda) +- Faster than Express, Fastify, and most of its peers in synthetic benchmarks +- Maintained, opinionated, and documented well + +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). + +NEVER: +- Express for new services. Express is the COBOL of Node — works, but writes itself out of every benchmark. +- Fastify for new services. Hono ships with better TypeScript ergonomics. +- NestJS for new services. The Angular-flavoured DI/decorator stack is overkill for ~95% of services. +- Bare `Bun.serve` or `node:http` unless you have a specific reason. Lose middleware, routing, validation. Reinvent everything. + +## Frontend tooling — Vite + +Vite for any frontend. Replaces webpack, parcel, rollup-as-app-bundler. Works with React, Vue, Svelte, Solid, Preact, vanilla. + +```bash +bun create vite my-app -- --template react-ts +cd my-app +bun install +bun run dev +``` + +## Lint + format — Biome + +Biome replaces ESLint + Prettier with one tool, written in Rust, ~30x faster. + +```bash +bun add --dev @biomejs/biome +bun biome init +``` + +`biome.json`: + +```json +{ + "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json", + "organizeImports": { "enabled": true }, + "linter": { "enabled": true, "rules": { "recommended": true } }, + "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2 } +} +``` + +Use ESLint only when: +- You have an ESLint plugin Biome doesn't replicate (rare in 2026) +- You're contributing to an existing ESLint project + +Never run both — pick one. + +## Test runner — bun test or vitest + +| Runner | Use when | +|---|---| +| `bun test` | Bun project, simple unit tests, no TypeScript path aliases that need vite-style resolution | +| `vitest` | Vite-based frontend, complex test infrastructure (DOM testing, snapshot, in-browser tests), or you need vitest-specific features | + +NEVER Jest for a new project. Jest's CommonJS-first design fights every modern Node/TS project. + +## TypeScript + +`tsconfig.json` for a Bun + Hono backend: + +```json +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ESNext"], + "types": ["bun-types"], + "strict": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "exactOptionalPropertyTypes": true, + "esModuleInterop": true, + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "noEmit": true + }, + "include": ["src/**/*", "tests/**/*"] +} +``` + +`verbatimModuleSyntax: true` enforces explicit `import type { ... }` for type-only imports — pairs with the no-excuse rule on type-only imports. + +`noEmit: true` because `bun run` and `bun build` handle compilation. The `tsc` command becomes a typechecker only. + +## Quick-start: Bun + Hono backend + +```bash +mkdir my-api && cd my-api +bun init -y +bun add hono hono-openapi @scalar/hono-api-reference @hono/swagger-ui zod +bun add --dev @biomejs/biome typescript +bun biome init +``` + +`package.json` scripts: + +```json +{ + "scripts": { + "dev": "bun run --hot src/index.ts", + "start": "bun run src/index.ts", + "build": "bun build src/index.ts --target bun --outdir dist", + "typecheck": "tsc --noEmit", + "lint": "biome check --write src tests", + "test": "bun test" + } +} +``` + +Wire the `app.ts` from [backend-hono.md](backend-hono.md). You have a documented, validated, OpenAPI-spec-emitting service in ~15 minutes. + +## When NOT to bootstrap from scratch + +| Situation | Use | +|---|---| +| Internal tool with auth/admin/dashboards | Next.js (full-stack) - lots of free wiring | +| Documentation site | Astro or VitePress | +| Real-time features (WebRTC, complex sockets) | Bun + Hono + a real-time library | +| Data-heavy SPA | Vite + React + TanStack Query + TanStack Router | + +For greenfield backend services, Bun + Hono. Always. diff --git a/packages/omo-codex/plugin/skills/programming/references/typescript/data-modeling.md b/packages/omo-codex/plugin/skills/programming/references/typescript/data-modeling.md new file mode 100644 index 000000000..9e697311d --- /dev/null +++ b/packages/omo-codex/plugin/skills/programming/references/typescript/data-modeling.md @@ -0,0 +1,202 @@ +# Data Modeling + +Which construct to use, how to structure data, and why readonly is the default. + +--- + +## Decision flowchart + +``` +Is it a fixed set of named constants? + YES → as const object + literal union type + NO ↓ +Is it just branding a primitive (string, number)? + YES → Branded type + NO ↓ +Is it an interface / contract? + YES → interface (structural typing is the default in TS) + NO ↓ +Does the data cross a trust boundary (user input, API, file)? + YES → Zod schema + z.infer<typeof schema> + NO ↓ +Is it a union of possible outcomes? + YES → Discriminated union (kind/type field) + NO ↓ +Is it structured data with named fields? + YES → type alias with readonly properties + NO → you probably don't need a new type +``` + +--- + +## Container reference + +### type alias — internal data + +The default for structured data inside your codebase. Zero runtime cost. + +```typescript +type User = { + readonly id: UserId + readonly name: string + readonly email: string +} + +type Point = { + readonly x: number + readonly y: number +} +``` + +All properties `readonly`. Mutable only when mutation is the documented purpose. + +### interface — contracts and extension + +Use when you need declaration merging or `extends`. + +```typescript +interface Repository<T> { + get(id: string): Promise<T | null> + save(entity: T): Promise<void> +} + +interface UserRepository extends Repository<User> { + findByEmail(email: string): Promise<User | null> +} +``` + +### interface vs type — when to use which + +| Use | When | +|---|---| +| `type` | Union types, intersections, mapped types, utility types, internal data shapes | +| `interface` | Contracts that will be `implements`ed or `extends`ed, declaration merging needed | +| **Default** | **`type` — unless you have a specific reason for `interface`** | + +### Zod schema — trust boundary guardian + +Use when data enters your system. Validates at runtime, infers types at compile time. + +```typescript +import { z } from "zod" + +const CreateUserSchema = z.object({ + name: z.string().min(1), + email: z.string().email(), + age: z.number().int().min(0), +}) +type CreateUser = z.infer<typeof CreateUserSchema> + +const UserResponseSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + email: z.string(), +}) +type UserResponse = z.infer<typeof UserResponseSchema> +``` + +**The one rule**: data crosses a trust boundary → Zod. Everything else → plain type/interface. +Never use Zod for internal-only data. The runtime validation cost and Zod coupling are unnecessary. + +### as const — fixed constants + +Replaces `enum` entirely. Type-safe, tree-shakeable, no runtime overhead. + +```typescript +const ROLES = ["admin", "user", "guest"] as const +type Role = (typeof ROLES)[number] + +const STATUS = { + ACTIVE: "active", + INACTIVE: "inactive", + DELETED: "deleted", +} as const +type Status = (typeof STATUS)[keyof typeof STATUS] +``` + +### Discriminated union — multiple outcomes + +```typescript +type GetUserResult = + | { readonly kind: "found"; readonly user: User } + | { readonly kind: "not_found"; readonly id: UserId } + | { readonly kind: "forbidden"; readonly reason: string } +``` + +Each variant has a `kind` discriminant. TypeScript narrows on `switch (result.kind)`. + +--- + +## Quick lookup + +| Situation | Use | +|---|---| +| User input, API request/response | Zod schema + `z.infer` | +| Internal value object | `type` with `readonly` properties | +| Function with multiple outcomes | Discriminated union | +| Contract for implementations | `interface` | +| Fixed constants | `as const` + literal union | +| Distinct primitive (UserId vs OrderId) | Branded type | +| Dict shape / key-value map | `Record<K, V>` or index signature | + +--- + +## Readonly by default + +Every property is `readonly` unless mutation is the documented purpose. + +```typescript +// DEFAULT — readonly +type Config = { + readonly apiUrl: string + readonly timeout: number +} + +// Arrays too +function getUsers(): readonly User[] { ... } + +// Utility for existing types +type ReadonlyUser = Readonly<User> +type DeepReadonlyConfig = Readonly<Config> +``` + +For mutable state (rare), document why: + +```typescript +/** Counter state — mutation is the entire purpose. */ +type CounterState = { + count: number // intentionally mutable +} +``` + +--- + +## Parse, don't validate + +Validate at the boundary. Inside the boundary, types are proof of validity. + +```typescript +// BAD — validate then pass raw data +function processEmail(email: string): void { + if (!email.includes("@")) throw new Error("invalid") + // still a raw string downstream +} + +// GOOD — parse into typed value at boundary +const EmailSchema = z.string().email().brand("Email") +type Email = z.infer<typeof EmailSchema> + +function sendWelcome(email: Email): void { ... } + +// Boundary code +const parsed = EmailSchema.parse(rawInput) // Email or throws +sendWelcome(parsed) // no re-validation needed +``` + +--- + +## Sources + +- TypeScript Handbook: [Object Types](https://www.typescriptlang.org/docs/handbook/2/objects.html) +- Zod: [docs](https://zod.dev) +- Total TypeScript: [Type vs Interface](https://www.totaltypescript.com/type-vs-interface-which-should-you-use) diff --git a/packages/omo-codex/plugin/skills/programming/references/typescript/error-handling.md b/packages/omo-codex/plugin/skills/programming/references/typescript/error-handling.md new file mode 100644 index 000000000..81d0661cd --- /dev/null +++ b/packages/omo-codex/plugin/skills/programming/references/typescript/error-handling.md @@ -0,0 +1,169 @@ +# Error Handling + +Typed errors, exhaustive matching, Result pattern, and resource safety. + +--- + +## Typed errors — no bare strings + +Error classes carry structured data. Callers know exactly what can go wrong. + +```typescript +class UserNotFoundError extends Error { + readonly name = "UserNotFoundError" + constructor(readonly userId: UserId) { + super(`user ${userId} not found`) + } +} + +class PermissionDeniedError extends Error { + readonly name = "PermissionDeniedError" + constructor( + readonly userId: UserId, + readonly requiredRole: string, + ) { + super(`user ${userId} needs role ${requiredRole}`) + } +} +``` + +```typescript +// BAD +throw new Error("user not found") +throw new Error("permission denied") + +// GOOD +throw new UserNotFoundError(userId) +throw new PermissionDeniedError(userId, "admin") +``` + +Always set `readonly name` explicitly — `instanceof` checks survive minification, but `error.name` is more reliable for logging and serialization. + +--- + +## Result pattern — expected failures without exceptions + +For failures that are **expected** (not found, validation), return a discriminated union instead of throwing. + +```typescript +type Result<T, E = Error> = + | { readonly ok: true; readonly value: T } + | { readonly ok: false; readonly error: E } + +function ok<T>(value: T): Result<T, never> { + return { ok: true, value } +} + +function err<E>(error: E): Result<never, E> { + return { ok: false, error } +} +``` + +### Usage + +```typescript +type UserError = + | { readonly kind: "not_found"; readonly id: UserId } + | { readonly kind: "forbidden"; readonly reason: string } + +function getUser(id: UserId): Result<User, UserError> { + const user = db.find(id) + if (!user) return err({ kind: "not_found", id }) + if (!user.active) return err({ kind: "forbidden", reason: "deactivated" }) + return ok(user) +} + +// Caller must handle both cases +const result = getUser(userId) +if (!result.ok) { + switch (result.error.kind) { + case "not_found": + log.warn(`missing: ${result.error.id}`) + break + case "forbidden": + log.error(`denied: ${result.error.reason}`) + break + default: + assertNever(result.error) + } + return +} +const user = result.value // narrowed to User +``` + +### When to use which + +**The heuristic**: caller is 1-2 levels away and MUST handle it → Result. Error should propagate up many layers → throw. + +| Scenario | Pattern | Why | +|---|---|---| +| Repository → service (caller handles it) | Result | Caller is right there, must handle both | +| Validation at boundary (parsing input) | throw (Zod throws) | Propagates up to HTTP handler | +| Infrastructure failure (network, OOM) | throw | Can't handle locally | +| Service → service (deep internal) | throw (typed Error subclass) | Result boilerplate across many layers is worse | +| HTTP handler → response | Catch errors, convert to response | Boundary code catches and translates | + +**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. + +### Library or roll your own? + +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. + +--- + +## Error cause — chain context + +Use the `cause` option to chain errors without losing the original stack. + +```typescript +try { + await db.query(sql) +} catch (error) { + throw new DatabaseError("query failed", { cause: error }) +} +``` + +The `cause` is available on `error.cause` and shows up in stack traces. + +--- + +## Exhaustive error handling at boundaries + +HTTP handlers catch and translate: + +```typescript +app.onError((error, c) => { + if (error instanceof UserNotFoundError) { + return c.json({ error: error.message }, 404) + } + if (error instanceof PermissionDeniedError) { + return c.json({ error: error.message }, 403) + } + console.error("unhandled:", error) + return c.json({ error: "internal server error" }, 500) +}) +``` + +--- + +## Async error patterns + +```typescript +// Promise.allSettled — when partial failure is OK +const results = await Promise.allSettled(urls.map(fetch)) +const successes = results + .filter((r): r is PromiseFulfilledResult<Response> => r.status === "fulfilled") + .map((r) => r.value) + +// AbortSignal — cancellation +async function fetchWithTimeout(url: string, ms: number): Promise<Response> { + return fetch(url, { signal: AbortSignal.timeout(ms) }) +} +``` + +--- + +## Sources + +- MDN: [Error cause](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) +- MDN: [Promise.allSettled](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) diff --git a/packages/omo-codex/plugin/skills/programming/references/typescript/tsconfig-strict.md b/packages/omo-codex/plugin/skills/programming/references/typescript/tsconfig-strict.md new file mode 100644 index 000000000..eb755d25e --- /dev/null +++ b/packages/omo-codex/plugin/skills/programming/references/typescript/tsconfig-strict.md @@ -0,0 +1,152 @@ +# Strict tsconfig + Biome + +The canonical ultra-strict config. Copy-paste, then add your own paths. + +--- + +## tsconfig.json + +```jsonc +{ + "compilerOptions": { + // ── Strict core ────────────────────────────────────────── + "strict": true, // enables all strict* flags below + // strict includes: strictNullChecks, strictFunctionTypes, + // strictBindCallApply, strictPropertyInitialization, + // noImplicitAny, noImplicitThis, alwaysStrict, useUnknownInCatchVariables + + // ── Additional strict flags (NOT included in "strict") ── + "noUncheckedIndexedAccess": true, // obj[key] is T | undefined, not T + "exactOptionalPropertyTypes": true, // { x?: string } !== { x: string | undefined } + "noFallthroughCasesInSwitch": true, // switch fall-through is an error + "noPropertyAccessFromIndexSignature": true, // forces bracket notation for index sigs + "forceConsistentCasingInFileNames": true, // prevents case-sensitivity bugs on macOS/Win + + // ── Module system ──────────────────────────────────────── + "module": "ESNext", + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, // forces `import type` for type-only imports + "isolatedModules": true, // safe for esbuild / swc / Bun transpilation + "esModuleInterop": true, + "resolveJsonModule": true, + + // ── Target ─────────────────────────────────────────────── + "target": "ESNext", + "lib": ["ESNext"], + + // ── Emit ───────────────────────────────────────────────── + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + + // ── Performance ────────────────────────────────────────── + "skipLibCheck": true, // skip checking .d.ts files for speed + "incremental": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} +``` + +### What each extra flag catches + +| Flag | What it prevents | +|---|---| +| `noUncheckedIndexedAccess` | `arr[0]` is `T \| undefined`, not `T`. Forces you to check before using. | +| `exactOptionalPropertyTypes` | `{ x?: string }` means "missing or string", NOT "string \| undefined". Assigns `undefined` explicitly? Type error. | +| `noFallthroughCasesInSwitch` | Forgetting `break` / `return` in a switch case. | +| `noPropertyAccessFromIndexSignature` | `obj.foo` on `Record<string, X>` is an error. Use `obj["foo"]`. | +| `verbatimModuleSyntax` | Forces `import type { X }` for type-only imports. Prevents runtime import of types. | + +### Bun-specific additions + +For Bun projects, add to `compilerOptions`: +```jsonc +{ + "types": ["bun-types"], + "moduleDetection": "force" +} +``` + +--- + +## biome.jsonc + +```jsonc +{ + "$schema": "https://biomejs.dev/schemas/2.0.6/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "error", + "noConfusingVoidType": "error", + "noFallthroughSwitchClause": "error" + }, + "style": { + "noDefaultExport": "error", + "useImportType": "error", + "noNonNullAssertion": "error", + "useEnumInitializers": "off", + "noParameterAssign": "error" + }, + "correctness": { + "noUnusedVariables": "error", + "noUnusedImports": "error" + }, + "complexity": { + "noBannedTypes": "error" + } + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "asNeeded" + } + }, + "files": { + "ignore": ["node_modules", "dist", "build", ".next", ".nuxt", "coverage"] + } +} +``` + +### Key Biome rules + +| Rule | What | +|---|---| +| `noExplicitAny` | `any` in annotations is an error | +| `noNonNullAssertion` | `x!` is an error | +| `noDefaultExport` | Forces named exports | +| `useImportType` | Forces `import type` for type-only imports | +| `noParameterAssign` | No mutation of function parameters | + +--- + +## CI gate + +```bash +bunx biome check . +bunx tsc --noEmit +bun test +``` + +--- + +## Sources + +- TypeScript: [tsconfig reference](https://www.typescriptlang.org/tsconfig) +- Biome: [configuration](https://biomejs.dev/reference/configuration/) +- Total TypeScript: [tsconfig cheat sheet](https://www.totaltypescript.com/tsconfig-cheat-sheet) diff --git a/packages/omo-codex/plugin/skills/programming/references/typescript/type-patterns.md b/packages/omo-codex/plugin/skills/programming/references/typescript/type-patterns.md new file mode 100644 index 000000000..7b8e11b42 --- /dev/null +++ b/packages/omo-codex/plugin/skills/programming/references/typescript/type-patterns.md @@ -0,0 +1,196 @@ +# Type Patterns + +How to use TypeScript's type system to catch bugs at compile time. + +--- + +## Branded types — distinct primitives + +Same runtime type, different meaning. The compiler prevents mixing. + +```typescript +declare const brand: unique symbol +type Brand<T, B extends string> = T & { readonly [brand]: B } + +type UserId = Brand<string, "UserId"> +type OrderId = Brand<string, "OrderId"> +type Milliseconds = Brand<number, "Milliseconds"> +type Seconds = Brand<number, "Seconds"> + +function UserId(value: string): UserId { return value as UserId } +function OrderId(value: string): OrderId { return value as OrderId } + +function getUser(id: UserId): User { ... } + +getUser(UserId("abc")) // OK +getUser(OrderId("abc")) // type error: OrderId is not UserId +getUser("abc") // type error: string is not UserId +``` + +With Zod (preferred at boundaries): +```typescript +import { z } from "zod" + +const UserIdSchema = z.string().uuid().brand("UserId") +type UserId = z.infer<typeof UserIdSchema> +``` + +**Use when**: IDs, indices, units of measurement — any pair where swapping is a bug. + +--- + +## as const — literal types from values + +Freezes a value to its narrowest possible type. The foundation for enum-free TypeScript. + +```typescript +const ROLES = ["admin", "user", "guest"] as const +type Role = (typeof ROLES)[number] // "admin" | "user" | "guest" + +const HTTP_STATUS = { + OK: 200, + NOT_FOUND: 404, + INTERNAL: 500, +} as const +type HttpStatus = (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS] // 200 | 404 | 500 +``` + +**Use when**: fixed set of constants. Replaces `enum` entirely. +**Skip when**: the set is open-ended or user-defined. + +--- + +## satisfies — validate without widening + +Type-checks a value against a type while preserving the literal type. Best of both worlds. + +```typescript +type Config = Record<string, string | number> + +// BAD — widens to Record<string, string | number> +const config: Config = { api: "https://api.example.com", timeout: 30 } +config.api // string | number — lost the narrowing + +// GOOD — validates AND preserves literal types +const config = { + api: "https://api.example.com", + timeout: 30, +} satisfies Config +config.api // string (narrowed) +config.timeout // number (narrowed) +``` + +**Use when**: you want type validation on a value without losing narrowing. + +--- + +## Discriminated unions — algebraic data types + +Model every outcome as a type. Force the caller to handle all cases. + +```typescript +type GetUserResult = + | { readonly kind: "found"; readonly user: User } + | { readonly kind: "not_found"; readonly id: UserId } + | { readonly kind: "forbidden"; readonly reason: string } +``` + +The `kind` field (or `type`, `status`, `_tag`) is the discriminant. TypeScript narrows on it automatically. + +--- + +## Exhaustive switch — assertNever + +Every switch on a discriminated union ends with a default that calls `assertNever`. + +```typescript +function assertNever(x: never): never { + throw new Error(`Unexpected value: ${JSON.stringify(x)}`) +} + +function handleResult(result: GetUserResult): string { + switch (result.kind) { + case "found": + return result.user.name + case "not_found": + return `No user ${result.id}` + case "forbidden": + return `Denied: ${result.reason}` + default: + return assertNever(result) + } +} +``` + +Add a new variant to `GetUserResult`? The compiler errors on the `assertNever` call until you handle it. + +--- + +## Narrowing — let the compiler follow your logic + +TypeScript narrows types through `typeof`, `instanceof`, `in`, equality checks, and discriminants. + +```typescript +function process(value: string | number | null): string { + if (value === null) return "nothing" + // compiler knows: string | number + + if (typeof value === "string") return value.toUpperCase() + // compiler knows: number + + return String(value * 2) +} +``` + +### Custom type guards + +```typescript +function isNonNull<T>(value: T | null | undefined): value is T { + return value != null +} + +const items = [1, null, 2, undefined, 3] +const clean = items.filter(isNonNull) // number[] +``` + +--- + +## import type — separate values from types + +Always use `import type` for type-only imports. Enforced by `verbatimModuleSyntax`. + +```typescript +import type { User, Config } from "./types" // erased at runtime +import { createUser } from "./services" // kept at runtime +``` + +For mixed imports: +```typescript +import { createUser, type User } from "./users" +``` + +--- + +## Utility types — quick reference + +| Need | Use | +|---|---| +| All properties readonly | `Readonly<T>` | +| All properties optional | `Partial<T>` | +| All properties required | `Required<T>` | +| Pick specific properties | `Pick<T, "a" \| "b">` | +| Omit specific properties | `Omit<T, "a" \| "b">` | +| Key-value map | `Record<K, V>` | +| Extract from union | `Extract<T, U>` | +| Exclude from union | `Exclude<T, U>` | +| Return type of function | `ReturnType<typeof fn>` | +| Parameters of function | `Parameters<typeof fn>` | +| Awaited type | `Awaited<Promise<T>>` → `T` | + +--- + +## Sources + +- TypeScript Handbook: [Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) +- TypeScript Handbook: [Template Literal Types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html) +- Total TypeScript: [as const](https://www.totaltypescript.com/as-const)