Skip to content

docs: teach app agents full-stack type safety instead of unknown / any #1199

Description

@vivek7405

Problem

WebJs sells full-stack type safety as a headline capability (README.md L48, the docs site /docs/typescript and /docs/server-actions), but the guidance an app-building AI agent actually reads does not teach it as a rule, and several of our own canonical examples model the exact anti-pattern instead.

Two gaps:

  1. No positive rule in the agent-facing guide. The scaffold's packages/cli/templates/AGENTS.md has a short "Type everything" section that says "Never reach for any or a loose as any cast", but it says nothing about unknown (the more common escape hatch an agent reaches for, because it looks safe), and it never teaches the positive patterns that make the escape hatch unnecessary: derive row types from the Drizzle schema, carry them over the .server boundary with import type, type routing files with PageProps / LayoutProps / RouteHandlerContext, and generate the route union with webjs types. The canonical skill reference .agents/skills/webjs/references/typescript.md documents each mechanism but never states the rule, so an agent skimming it has no reason to prefer the typed form.

  2. Our own examples teach the anti-pattern. An agent copies the example far more reliably than it follows the prose. Today the examples say:

    • export default function RootLayout({ children }: { children: unknown }) in at least 10 places (the scaffold gallery, the docs site, the skill references, packages/cli/templates/scripts/clear-gallery.mjs), even though @webjsdev/core exports LayoutProps whose children is a TemplateResult (packages/core/src/routes.d.ts L136-139). This is the single most-copied unknown in the whole corpus and it appears in the ROOT LAYOUT, the first file a new app opens.
    • export async function createPost(input: unknown) in the docs site's flagship "Full-Stack Type Safety" section (website/app/docs/typescript/page.ts L86) and in /docs/server-actions (L310). The section whose whole point is "TypeScript sees the real function signature" demonstrates a signature that carries no information, and the following comment even says "TypeScript knows createPost accepts (input: unknown)".
    • export const validate = (input: any) in .agents/skills/webjs/references/data-and-actions.md L108 and website/app/docs/backend-only/page.ts L155.
    • { id: crypto.randomUUID() as any, ... } in the optimistic-UI example, in AGENTS.md L372 and .agents/skills/webjs/references/optimistic-ui.md L32-34.

Net effect: a WebJs app built by an agent ends up with unknown-typed layouts and any-typed action inputs, which silently discards the framework's main type-safety selling point, and webjs check cannot catch it (it is a convention, not a correctness rule).

Design / approach

Docs and examples only. No framework code, no new webjs check rule (this is guidance a sensible app could legitimately do differently in a specific spot, so per the AGENTS.md conventions-vs-checks split it belongs in prose, not in the tool).

Two halves:

A. State the rule, with the positive patterns. Add a section to .agents/skills/webjs/references/typescript.md (the canonical full reference, copied verbatim into every scaffolded app) that says: at every app boundary, the type is derivable, so derive it. Cover the concrete ladder an agent should climb before reaching for an escape hatch:

  • DB rows: typeof posts.$inferSelect / $inferInsert from db/schema.server.ts, re-exported as a named type, carried into shipping components with import type (the existing carrier rule).
  • Action inputs and outputs: a named interface for the input, ActionResult<T> for the return, never input: unknown / input: any.
  • validate: type its argument as the action's input type (it receives the action's first argument) rather than any.
  • Routing files: PageProps<'/blog/[slug]'>, LayoutProps, RouteHandlerContext, plus webjs types for the route union.
  • Reactive props: prop<Student>(Object) / prop<Tag[]>(Array), not prop(Object) with a cast at the read site.
  • Optimistic temp rows: model the pending shape (Todo & { pending?: boolean }) instead of as any on the temp id.
  • The narrow, legitimate uses of unknown that should REMAIN: a genuinely untrusted external payload at the moment before it is narrowed (a route.ts await req.json(), a third-party webhook body, a catch (e: unknown)), where unknown is the correct type and the very next line narrows it. The rule is "never unknown as a substitute for a type you could write", not "never unknown".

Then reference it, briefly, from the surfaces an agent reads first: the repo AGENTS.md TypeScript invariant area, .agents/skills/webjs/SKILL.md, and the scaffold's packages/cli/templates/AGENTS.md "Type everything" section (extend it to name unknown and point at the skill reference for the ladder).

B. Fix every example that models the anti-pattern, since a contradicted rule is a dead rule. That is the { children: unknown } sweep to LayoutProps, the input: unknown action signatures, the validate = (input: any) pair, and the optimistic as any.

Implementation notes (for the implementing agent)

Where to edit.

Rule text (new prose):

  • .agents/skills/webjs/references/typescript.md, "Full-stack type safety" section (starts L78). The new subsection goes here, after "Server actions type-check automatically" and before / around "The carrier rule" (L~118), since the carrier rule is one rung of the ladder.
  • .agents/skills/webjs/SKILL.md L48 routes to references/typescript.md; the summary line there should mention the rule so the router surfaces it.
  • AGENTS.md invariant 10 area (the TypeScript invariant) and/or the references/typescript.md row in the detail-docs table L22.
  • packages/cli/templates/AGENTS.md "Type everything (all templates)" section, L32-39. It already bans any; extend to unknown and point at .agents/skills/webjs/references/typescript.md.

Example fixes ({ children: unknown } to LayoutProps, import the type from @webjsdev/core):

  • packages/cli/templates/gallery/app/examples/layout.ts L7
  • packages/cli/templates/gallery/app/features/layout.ts L48 (also takes url, so LayoutProps covers url too, check the shape in packages/core/src/routes.d.ts before widening)
  • packages/cli/templates/gallery/app/features/auth/dashboard/layout.ts L10
  • packages/cli/templates/scripts/clear-gallery.mjs L194 (this is a GENERATED string inside a script, so the fix must survive the escaping, see landmines)
  • website/app/docs/getting-started/page.ts L85, website/app/docs/routing/page.ts L113 + L125, website/app/docs/ssr/page.ts L268, website/app/docs/styling/page.ts L31 + L276, website/app/docs/layout.ts L155 (this last one is the docs site's REAL layout, not a doc snippet, so it is a live code change; webjs check and a boot must still pass)
  • .agents/skills/webjs/references/routing-and-pages.md L44

Example fixes (action input / validate / optimistic):

  • website/app/docs/typescript/page.ts L86 + the comment at L96
  • website/app/docs/server-actions/page.ts L310
  • website/app/docs/backend-only/page.ts L155 (validateUser = (input: any))
  • .agents/skills/webjs/references/data-and-actions.md L108 (validate = (input: any))
  • .agents/skills/webjs/references/optimistic-ui.md L32-34 and AGENTS.md L372 (crypto.randomUUID() as any), which must stay in sync with each other

Landmines / gotchas.

  • The scaffold skill is copied, not duplicated. packages/cli/lib/create.js L640-660 copies .agents/skills/webjs/ from the repo root (or a prepack bundle produced by scripts/sync-scaffold-skill.mjs) into every generated app. So edit the repo-root canonical ONCE; do NOT hand-copy it under packages/cli/templates/. The bundle is gitignored in the monorepo.
  • Bun prose rewriting. That same copy runs bunifyProse over the skill's markdown for --runtime bun, EXCLUDING references/runtime.md. New prose containing runnable npm commands will be rewritten; keep the new text's commands in the canonical npm form (npx webjsdev types), never a hand-written bun form.
  • Touching packages/cli/templates/gallery/** is the scaffold surface, not just docs. Per the webjs-doc-sync skill, that slice belongs to webjs-scaffold-sync and carries a mandatory generate + boot + webjs check verification (the generators emit strings, so an escaping bug only shows in a freshly generated app). Run webjs create into a temp dir, boot it, and run webjs check before calling this done.
  • clear-gallery.mjs L194 emits source as a STRING. A ${...} or a backtick added there is evaluated by the script's own template literal unless escaped. Verify by running the script's output through a real generated app, not by reading the diff.
  • Prose-punctuation invariant (AGENTS.md invariant 11) applies to every markdown and doc-page line added: no em-dashes, no space-surrounded hyphen or semicolon as a pause, WebJs capitalized in prose but lowercase webjs as a code token. The .claude/hooks/block-prose-punctuation.sh hook enforces it on NEW content.
  • **No backticks inside html\...`(invariant 9)** applies to the docs-site.tspages, whose content lives insidehtmltemplates. The existing pages escape<as<inside
    ` blocks; match that convention exactly or the page 500s or renders wrong.
  • LayoutProps.children is typed TemplateResult (packages/core/src/routes.d.ts L136-139). If any of the layouts being converted passes something else through (a string, an array), fix the site rather than widening the type.

Invariants to respect. AGENTS.md invariants 8 (only the root layout writes the document shell, so a converted non-root layout must stay shell-free), 9, 10, 11. The conventions-vs-checks split: this is prose guidance, so do NOT add a webjs check rule for it.

Tests + doc surfaces.

  • Doc surfaces per webjs-doc-sync: AGENTS.md + .agents/skills/webjs/ (SKILL.md + references/typescript.md, references/data-and-actions.md, references/optimistic-ui.md, references/routing-and-pages.md), the docs site (website/app/docs/typescript, /server-actions, /backend-only, /routing, /ssr, /styling, /getting-started), and the scaffold (packages/cli/templates/AGENTS.md + gallery/**). llms.txt / llms-full.txt are live-generated, so no manual edit.
  • Tests: this is docs + templates, so the gates are webjs check on the website app and on a freshly generated app, plus npm test for the scaffold-template tests under packages/cli/test/ (there are existing scaffold tests that assert generated-file content; check whether any asserts the { children: unknown } string and update it). Boot the website locally to confirm /docs/typescript, /docs/routing, /docs/ssr, /docs/styling still render after the <pre> edits.

Acceptance criteria

  • .agents/skills/webjs/references/typescript.md states the rule (derive the type at every app boundary; unknown / any only for a genuinely untrusted payload about to be narrowed) and shows the concrete ladder: $inferSelect row types, import type across the .server boundary, a named action-input interface, ActionResult<T>, validate typed to the input, PageProps / LayoutProps / RouteHandlerContext, webjs types, prop<T>().
  • The rule is referenced from AGENTS.md, .agents/skills/webjs/SKILL.md, and the scaffold's packages/cli/templates/AGENTS.md "Type everything" section (which now names unknown, not just any).
  • No { children: unknown } remains in any layout example or live layout across the scaffold gallery, the docs site, the skill references, and clear-gallery.mjs; each uses LayoutProps.
  • No input: unknown / input: any remains in a server-action or validate example on the docs site or in the skill references.
  • The optimistic-UI example no longer uses as any for the temp id, and AGENTS.md matches references/optimistic-ui.md.
  • A freshly generated app (webjs create, both templates) boots and passes webjs check.
  • The website app passes webjs check and every edited docs page renders.
  • npm test passes, including any scaffold test asserting generated-file content.

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation

Type

No type

Projects

Status
In progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions