create-strict-typescript
v0.2.1
Published
Curated starter toolkit with a strict TypeScript baseline (Biome + Oxlint + tsgo). Ships lib, CLI (Node & Bun), and TanStack Start + oRPC templates.
Downloads
389
Maintainers
Readme
create-strict-typescript
Curated starter toolkit with a strict TypeScript baseline (Biome + Oxlint + tsgo), plus opinionated templates for full-stack apps, CLIs, and libraries — and per-feature agent guidance that ships inside every scaffolded project.
Thesis
Ambiguity is the tax you pay on every line of code that hasn't been written yet.
The faster a bad idea turns into a red squiggle, the less time is spent running it, reviewing it, committing it, and shipping it. That matters for humans and it matters even more for agents — LLMs navigate a codebase by treating type errors, lint errors, and failed tests as the ground-truth signal. If the type system is permissive, an agent will cheerfully produce code that compiles but is wrong; if the type system is strict, the same agent self-corrects inside one loop. Constrain the search space and quality goes up — for both parties at the keyboard.
This starter bakes in the strictest settings we've found that still compose across real projects. The defaults below are enabled everywhere — template or init, frontend or backend.
noUncheckedIndexedAccess—arr[0]isT | undefined, notT. No silentundefined.foo()at runtime.exactOptionalPropertyTypes—{ x?: string }means the key can be absent, not present-and-undefined. Catches the surprising case whereobj.x = undefinedchanges behavior.useUnknownInCatchVariables—catch (err)bindserrasunknown. You must narrow before touching it.noImplicitReturns+noFallthroughCasesInSwitch— every branch either returns or breaks; no accidental fall-through.verbatimModuleSyntax(in CLI/lib templates) — you writeimport type { T }when you mean it. No runtime/type drift.isolatedModules— every file must be a module. Required for transpile-only toolchains (swc, esbuild, tsgo).- Oxlint
typescript/no-unnecessary-condition— kills impossible branches (if (x)whenxisnevernullable). - Oxlint
typescript/restrict-plus-operands+restrict-template-expressions— no accidental coercion of objects to[object Object]in strings or addition. - Oxlint
eslint/no-unused-varswith_escape hatch — unused symbols fail, but deliberate drops (const [_, x] = pair) opt out. - Biome
style/useBlockStatements—if (x) doThing();is rejected, you write braces. Consistent diff shapes, fewer merge conflicts, no dangling-else traps. - lint-staged + husky pre-commit — every staged file is formatted + lint-fixed on commit. Nothing broken lands.
A concrete example
// Without the baseline:
function getFirst(items: string[]): string {
return items[0]; // ← returns `string`, compiles, blows up at runtime if empty
}
getFirst([]).toUpperCase();
// With the baseline (noUncheckedIndexedAccess):
function getFirst(items: string[]): string {
return items[0]; // ← TS error: Type 'string | undefined' is not assignable to 'string'
}
// You're forced to handle the empty case — at the keyboard, not in prod.The philosophy: prefer failing fast at the keyboard over writing code that "works until it doesn't". An LLM (or teammate) reading this codebase can't accidentally trust a value that the type system hasn't proved. That's the whole point.
Quick start
pnpm create strict-typescript my-app
bun create strict-typescript my-app
npm create strict-typescript my-appPick a template interactively, or name one up-front:
pnpm create strict-typescript my-app --template tanstack
pnpm create strict-typescript my-cli --template cli-bun
pnpm create strict-typescript my-lib --template libGot an existing project you want to retrofit with the same baseline? Run init mode from inside it:
cd existing-project
pnpm create strict-typescript --initWhat's in the baseline
Every template — and init mode — layers on:
- Biome — formatter + linter + import sorting, one tool
- Oxlint — the fast Rust linter, with type-aware rules via the
oxlint-tsgolintbackend - tsgo — Go-native TypeScript compiler (preview) for fast typechecking
- Knip — dead code + unused export detection
- husky + lint-staged — pre-commit hook that runs Biome + Oxlint on staged files
- Strict
tsconfig.jsonflags —noUncheckedIndexedAccess,exactOptionalPropertyTypes,useUnknownInCatchVariables, plus everystrict: trueflag
Full set of compiler options added:
{
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"useUnknownInCatchVariables": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"allowJs": true
}Framework-specific settings (jsx, lib, moduleResolution, paths, target, module, types) are preserved when they already exist — only the strict-level flags are layered on.
Agent guidance (skills)
Every generated project ships with focused Agent Skills under .claude/skills/ — one topic per file, auto-discovered by Claude Code, OpenAI Codex CLI, Cursor, Gemini CLI, and GitHub Copilot (they share the Agent Skills open standard). An AGENTS.md is also generated at the root following the AGENTS.md convention used by Windsurf, Amp, Devin, and others.
Which skills ship depends on what you scaffolded:
| Skill | Shipped when |
|---|---|
| strict-typescript | always (idiomatic fixes for noUncheckedIndexedAccess, exactOptionalPropertyTypes, etc.) |
| lint-stack | always (which tool catches what; when to run fix vs fixunsafe) |
| pre-commit | --husky (default) — hook mechanics + when --no-verify is acceptable |
| tanstack-start | tanstack template (file-based routing, loader vs server-fn vs oRPC) |
| orpc-patterns | tanstack template (adding a procedure, React Query bindings) |
| auth-seam | --auth=placeholder (the single-file seam for swapping providers) |
| supabase-auth | --auth=supabase (cookie adapter, PKCE callback, env vars) |
| ui-daisyui | --ui=daisyui (theme switching, class conventions) |
| heroui-ssr | --ui=heroui (SSR noExternal invariant, Provider wrap) |
| drizzle-workflow | --db=drizzle (schema → generate → push vs migrate; db:studio) |
| capacitor-build | --capacitor (webDir invariant, VITE_SPA_MODE toggle, safe-area CSS) |
| citty-commands | cli-node / cli-bun (subcommand tree, arg types) |
| lib-publishing | lib (exports shape, pre-publish checklist) |
When an LLM opens the scaffolded project later, it sees the exact conventions you picked — no guessing at how oRPC is wired, where the auth seam is, or why vite.config.ts has SSR externals pinned.
Templates
tanstack — full-stack TanStack Start + oRPC
Scaffold-time prompts:
| Prompt | Options |
|------------|-----------------------------------------------|
| Auth | none / placeholder (default) / supabase |
| UI library | none / DaisyUI / HeroUI v3 |
| Database | none / Drizzle + SQLite |
| Capacitor | no (default) / yes |
- Includes a working
/route that fetches via oRPC + React Query - oRPC handler mounted at
/api/v1/rpc/$ placeholderauth ships the full oRPC auth shape with anauthServicestub — swap the stub for real auth in one filesupabasefills the stub with a working@supabase/ssrcookie-adapter flow +/api/v1/auth/callback- Capacitor adds
capacitor.config.ts, build scripts, and aMOBILE.mdnote about runningcap add ios/cap add androidyourself
cli-node — publishable Node CLI
- citty for argv parsing + subcommands
- tsdown for fast ESM builds
- Sample
hellocommand binfield pre-wired
cli-bun — Bun CLI
- Runs
.tsdirectly, no build step for dev build:compileproduces a single-file native binary viabun build --compilecitty(runtime-agnostic) for commands- Uses
Bun.versionetc. where it makes a meaningful difference
lib — minimal TypeScript library
type: module,exportsmap,filesfield- Optional
tsdownbuild withdts: true(default on) - No framework coupling
init — overlay on an existing project
Runs in the current directory:
- Merges strict flags into your existing
tsconfig.json(never overwritesjsx/lib/moduleResolution/paths) - Creates
biome.json,.oxlintrc.json,knip.json,.vscode/settings.json,.husky/pre-commitif they don't exist - Merges core scripts + devDeps into
package.json(never overwrites existing scripts) - Auto-enables Oxlint's React rules if React is detected in deps
- Idempotent — second run is a no-op
CLI options
pnpm create strict-typescript [dir] [options]
--template <id> lib | cli-node | cli-bun | tanstack
--init Overlay baseline onto cwd instead of scaffolding a new project
--pm <pm> Force package manager (npm | pnpm | yarn | bun)
--yes, -y Skip prompts, use defaults
--no-install Don't run <pm> install after scaffolding
--no-husky Skip husky + lint-staged
--no-knip Skip knip
--no-tsgolint Skip type-aware oxlint backend (alpha)
--no-tsgo Use tsc instead of tsgo for typecheck
--version, -v Print version
--help, -h Print helpExtending
Templates live in src/templates/<id>/ — each one is a folder with a template.ts describing prompts + feature overlays, plus a files/ directory of verbatim-copied files and (optionally) a features/<feature-name>/ directory per overlay.
Add a template:
- Create
src/templates/your-template/{template.ts, files/, features/*} - Register it in
src/templates/registry.ts - That's it — the CLI core never touches framework-specifics.
Under the hood
- Prompts: @clack/prompts (v1.2 — used by
create-vite,create-astro,create-t3-app) - Colors: picocolors (~400 bytes, ~8M ops/sec)
- Spinners: clack's built-in
tasks()runner - PM detection: package-manager-detector
tsconfig.jsonmerging: comment-json (preserves comments + trailing commas)- Everything bundled with tsdown (zero-config, ESM-first)
License
MIT
