Summary
stx typecheck now checks {{ }} and directive expressions against the enclosing scope (#1852). Where the server declares the data, those get real types — @foreach(rows as row) makes {{ row.total_vists }} an error against a typed Row.
Where the client declares it, they get nothing, because every runtime global is declared any:
// packages/stx/src/stx-virtual-ts.ts — runtimeGlobalDeclarations()
declare var state: any
declare var derived: any
declare var useFetch: any
// …66 moreSo const count = state(0) gives count: any, and every template expression reading it is unchecked. {{ count + 1 }}, {{ user.nmae }} where user is a signal, :if="cart.totl" — all compile.
This is the same class as the server-side gap #1852 was filed for, one layer in: the annotation exists for a human and is verified by nothing.
Why it wasn't done in #1852
Two reasons, and the second is the interesting one.
1. stx.d.ts types these richly already, but including it drags in the built declarations — and dist/composables.d.ts has been syntactically invalid at least once (navigate: (path: string)) => unknown;). tsc aborts on a syntax error in a lib file — skipLibCheck only suppresses semantic ones — so the checker reported nothing at all about the user's code. Generating from STX_RUNTIME_GLOBALS keeps the tool working regardless of the state of the build output. Any fix here has to keep that property.
2. Templates auto-unwrap signals, so the same name has two types depending on where it appears:
<div :if="flag">…</div> <!-- reads the VALUE -->
<button @click="flag.set(true)"> <!-- reads the SIGNAL -->A single declared type cannot satisfy both. Options, roughly in order of appeal:
- Declare template-scope bindings as
T & Unwrap<T>— an intersection that permits both the value operations and.set(). Cheap, no per-attribute logic, but produces odd types in hover. - Type per attribute kind: value type for
:if/:show/x-text/{{ }}, signal type for@-handlers. More correct, needs the extractor to thread the attribute through to the declaration. - Type the runtime globals precisely and leave the unwrap unmodelled, accepting false positives on the
@-handler side.
The measurement bar from #1852 applies: the corpus run went 400 → 1 diagnostic over 2072 expressions, and the one survivor is a genuine bug. Any change here should be re-measured the same way (packages/stx/test/typecheck/stx-virtual-ts.test.ts documents the corpus method) — a template checker that invents errors is worse than none, because a muted CI gate catches nothing.
Ask
- Type
STX_RUNTIME_GLOBALSfor the checker without depending ondist/being valid. - Pick a model for the auto-unwrap split and encode it in the extractor.
- Re-measure against the corpus; the gate stays at zero false positives.