A client-block declaration that has a type annotation but no initializer — let rows!: () => Row[] — is never extracted, so the binding does not exist in the client scope. Every template expression reading it silently evaluates to nothing. No console error, no diagnostic, the element just renders empty.
What it cost
Adding types to a production composer view. The declaration went from
let providerRows = nullto
let providerRows!: () => ProviderRow[]and nothing else changed — the assignment (providerRows = derived(…)) and the template (x-for="row in providerRows()") were untouched. Rendered, in a real browser against the same API responses:
| provider rows rendered | |
|---|---|
let providerRows = null | 2 |
let providerRows!: () => ProviderRow[] | 0 |
Zero console errors in both. The page looks fine; the channel picker is simply gone.
Nothing catches it
stx typecheckreports 0 errors on the repro below, for both forms.stx codemodreports nothing.tsc, lint and the unit suite are all green — the failure only exists in the rendered DOM.
This is the "answered 200 and did nothing" class from the August sweep notes, reached through the type system.
Repro
<script client>
let withInit = null
let noInit!: () => string[]
withInit = () => ['a', 'b']
noInit = () => ['a', 'b']
</script>
<template>
<ul data-with-init><li x-for="x in withInit()" :text="x"></li></ul>
<ul data-no-init><li x-for="x in noInit()" :text="x"></li></ul>
</template>data-with-init renders two <li>. data-no-init renders none.
Cause
packages/stx/src/variable-extractor.ts has two declaration branches and this form falls between them:
// 129 — requires an initializer
const match = line.match(/(?:const|let|var)\s+(\w+)\s*=/)
// 136 — declaration-only, but the line must end right after the name
else if (/^\s*(?:const|let|var)\s+\w+\s*;?\s*$/.test(line)) {let noInit!: () => string[] has no =, so 129 misses, and it does not end after the name, so 136 misses too.
Line 202 already handles the annotated-with-initializer form by stripping the annotation:
/^(\s*(?:export\s+)?(?:const|let|var)\s+)(\w+|\{[^}]+\}|\[[^\]]+\])\s*:\s*[^=]+(\s*=)/gmso const x: T = … is fine. It is specifically annotation-without-initializer that is dropped.
Fix
Widen the declaration-only branch to allow a definite-assignment ! and a type annotation, i.e. accept let x, let x!: T, and let x: T alongside the initialized forms.
Worth considering a diagnostic as well: a template expression calling an identifier that no declaration published is currently indistinguishable from one that legitimately evaluates to nothing, and that is what makes this cost an afternoon rather than a minute. Related in spirit to #1890, which was the same extractor dropping a different declaration shape.