Summary
There is no way to type-check a .stx file outside an editor. tsc treats .stx as an unknown extension, the stx CLI has no typecheck/check command, and the only checking that exists is a VS Code tsserver plugin that concatenates all script blocks into one virtual file and then filters unknown-identifier errors away. The practical consequence is that a page's <script server> block, its <script client> block and every expression are unverified, and the only way to get type coverage on template logic is to move that logic into a .ts file on the tsconfig include path.
Impact (real app)
analyticshq is an SSR-first app whose dashboard page carries ~500 lines of SQL and data shaping in resources/views/dashboard.stx:220-758. We physically split that page across three files by the criterion "can tsc see it" rather than by cohesion.
resources/views/dashboard.stx:8-13 records the decision at the top of the server block:
The pure formatters and the channel classifier live in a real .ts module rather than in this block, because tsc cannot see inside a .stx file -- an annotation written here is erased by the transpiler and checked by nothing, while app/Support is on the tsconfig include path.
So 119 lines moved to app/Support/dashboard-format.ts purely for checkability (its header at :4-8 repeats the reason). tsconfig.json:35-43 confirms the split — include lists types/**/*.ts, resources/stores/**/*.ts, app/Models, app/Services, app/Support; resources/views is never included, because including it would do nothing.
For what could not be moved (request-scoped helpers, chart geometry consumed by the template) we hand-wrote 56 lines of ambient interfaces in types/dashboard.d.ts:16-56, with an honest disclaimer at :10-13:
tsc cannot see inside .stx, so these annotate the page for a human and an editor and are verified by nothing. They are still worth writing, but a wrong shape here is a real defect that no build step will catch.
Net: 119 lines relocated against cohesion, 56 lines of types that verify nothing, ~500 lines of query/shaping code with zero coverage, and no CI gate that would catch a renamed column or a changed row shape.
Evidence in stx
- No CLI entry point.
packages/stx/bin/cli.tsregisters 28 commands (docs,dev,a11y,build,start,deploy,bundle,test,init,format,doctor,debug,status,watch,analyze,screenshot,interactive,story*,compile, …). There is notypecheckorcheck. - No published checker package.
packages/containsbun-plugin,router,components,vscode,stx, … and nothing resemblingstx-tscor a language server. - The type machinery that exists is unexported.
packages/stx/src/typescript-templates.ts:601(typeCheckExpression) and:686(validateTemplateTypes, which walksexpressions) are real functions, butpackages/stx/src/index.tsnever re-exports./typescript-templates, so no consumer can call them. - The editor path is lossy in two specific ways.
packages/vscode/src/typescript-stx-plugin.ts:19-26builds the virtual TS file by appending every<script>body in the file, server and client together — two blocks legitimately declaring the same local name produce a spurious redeclaration, and a client block silently "sees" server bindings it cannot reach at runtime. Then:157-179proxiesgetSemanticDiagnosticsand drops every TS2304 (Cannot find name) whose message mentions one of a hardcoded list (props,state,derived,effect,onMount,loop, …), which also suppresses genuine typos in template expressions. packages/stx/package.json:115isbun tsc -p tsconfig.src.json --noEmit— that checks stx's own.tssources, not templates.
Prior art
- Vue:
vue-tsc(Volar) — CLI, runs in CI, checks the SFC script AND template expressions against the component's props. - Svelte:
svelte-check. - Angular:
strictTemplatesintsconfig.json, enforced byng build. - Astro:
astro check. - React/TSX: no special tooling needed — it is TypeScript.
All of them provide a virtual-file mapping with source positions, so a template error reports at the right line in the .stx/.vue file.
Ask
- Ship
stx typecheck [patterns...]as a CLI command that exits non-zero on error, so it can be a CI step and a pre-commit hook. - Build the virtual TS file per script BLOCK, not per file: a
<script server>and a<script client>in the same page are separate scopes and must not share a declaration space. - Declare stx's runtime globals as ambient types for the client scope (derived from
STX_RUNTIME_GLOBALSinpackages/stx/src/runtime-globals.ts, which is already the single source of truth) instead of filtering TS2304 away intypescript-stx-plugin.ts:157-179. Same for the server scope's injected context (params,query,cookies,host,ip,setResponseStatus— seepackages/bun-plugin/src/serve.ts:1281-1330). - Type-check
expressions and directive attribute expressions (:if,:for,@click) against the enclosing scope —packages/stx/src/typescript-templates.ts:686already walks them; export it and give it a real checker instead of the hand-rolled member-access walk at:601-680. - Emit a source map from the virtual file back to
.stxoffsets so errors point at the template line, and reuse the same mapping in the VS Code plugin so editor and CI agree.