<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Oscar Green</title>
    <description>The latest articles on DEV Community by Oscar Green (@oscar_green_2836be55d3b02).</description>
    <link>https://dev.to/oscar_green_2836be55d3b02</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3119068%2Ff516b26b-d3e2-4391-9c14-d0e122ae11bf.png</url>
      <title>DEV Community: Oscar Green</title>
      <link>https://dev.to/oscar_green_2836be55d3b02</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oscar_green_2836be55d3b02"/>
    <language>en</language>
    <item>
      <title>tsc-correctness != runtime-correctness</title>
      <dc:creator>Oscar Green</dc:creator>
      <pubDate>Sun, 24 May 2026 12:28:12 +0000</pubDate>
      <link>https://dev.to/oscar_green_2836be55d3b02/tsc-correctness-runtime-correctness-3ol5</link>
      <guid>https://dev.to/oscar_green_2836be55d3b02/tsc-correctness-runtime-correctness-3ol5</guid>
      <description>&lt;p&gt;Here is a TypeScript error you have probably seen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/Header.tsx:3:10 - error TS2614: Module '"./logo.svg"' has no exported
  member 'ReactComponent'. Did you mean to use 'import Logo from "./logo.svg"'
  instead?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;tsc helpfully tells you the fix. Your editor offers it as a one-click Quick Fix. An LLM doing code repair will reliably take it. After the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Logo&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./logo.svg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;//                            ← tsc is now green&lt;/span&gt;
&lt;span class="c1"&gt;//                            ← the dev server now crashes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fixed file type-checks. The app is broken. &lt;code&gt;Logo&lt;/code&gt; is now the asset URL string, not a React component, so &lt;code&gt;&amp;lt;Logo /&amp;gt;&lt;/code&gt; blows up with "Logo is not a function" the moment the page renders.&lt;/p&gt;

&lt;p&gt;This is the gap LLM-driven code repair walks into every day, and the gap &lt;a href="https://github.com/owgreen-dev/tsfix" rel="noopener noreferrer"&gt;tsfix&lt;/a&gt; is built around.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three characters
&lt;/h2&gt;

&lt;p&gt;Three things shape the LLM-repair failure in this case, and they all have to be in the room to see why it happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The library author.&lt;/strong&gt; &lt;code&gt;vite-plugin-svgr&lt;/code&gt; v4 changed how you import an SVG as a React component. The default import is now the asset URL, and you have to opt in to the React-component import with a &lt;code&gt;?react&lt;/code&gt; query suffix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Logo&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./logo.svg?react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reasonable choice for them: it makes the plugin's default behavior match what every other Vite asset import does, and the &lt;code&gt;?react&lt;/code&gt; suffix is explicit. Breaking change, but small. Their migration guide says exactly this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The TypeScript team.&lt;/strong&gt; TS doesn't know about Vite plugins. All it sees is a &lt;code&gt;*.svg&lt;/code&gt; ambient module declaration. If your declaration says &lt;code&gt;export const ReactComponent: ...&lt;/code&gt; and you import &lt;code&gt;{ ReactComponent }&lt;/code&gt;, that's a named import that doesn't exist — TS2614. Its Quick Fix logic looks for plausible alternative imports from the same module, finds the &lt;code&gt;default&lt;/code&gt; export, and suggests &lt;code&gt;import Logo from "./logo.svg"&lt;/code&gt;. From TS's seat, that's a clean suggestion. It makes the program type-check.&lt;/p&gt;

&lt;p&gt;TS is right about types and wrong about runtime semantics. It has no way to be otherwise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The LLM.&lt;/strong&gt; Trained on a corpus that is, overwhelmingly, "code that compiles." Pre-v4 vite-plugin-svgr exported &lt;code&gt;ReactComponent&lt;/code&gt; as a named member — millions of training tokens still say so. v4 docs exist but they're a thin slice of the data. When tsc says "use the default import instead," the LLM agrees. It would have suggested the same fix on its own.&lt;/p&gt;

&lt;p&gt;The LLM is not being lazy. It is doing exactly what its training distribution and the compiler's hint tell it to do. They both happen to be wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is not the LLM. The fix is the prompt context.
&lt;/h2&gt;

&lt;p&gt;tsfix reads your &lt;code&gt;package.json&lt;/code&gt; on every Layer-2 invocation. If it sees &lt;code&gt;vite-plugin-svgr&lt;/code&gt; at v4 or higher in your dependencies, it injects this into the system prompt headline, &lt;em&gt;before&lt;/em&gt; the model sees the errored file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### library-migrations&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; vite-plugin-svgr: v4 requires the &lt;span class="sb"&gt;`?react`&lt;/span&gt; query suffix to import an SVG
  as a React component. &lt;span class="sb"&gt;`import Logo from "./logo.svg"`&lt;/span&gt; returns the asset URL.

&lt;span class="gu"&gt;### task&lt;/span&gt;
Library migration: vite-plugin-svgr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No fine-tuning, no agent loop, no retrieval pipeline. A registry lookup against &lt;code&gt;package.json&lt;/code&gt; and four lines of prompt.&lt;/p&gt;

&lt;p&gt;On our benchmark, the &lt;code&gt;?react&lt;/code&gt;-migration case goes from &lt;strong&gt;0/3 to 3/3&lt;/strong&gt; with this change. The model already knew about &lt;code&gt;?react&lt;/code&gt;; it just needed permission to override tsc's hint.&lt;/p&gt;

&lt;p&gt;The same shape works for other libraries whose major bumps generate confidently wrong tsc fixes: &lt;code&gt;next@15&lt;/code&gt; (params and searchParams are now Promises and must be awaited), &lt;code&gt;ai@v3&lt;/code&gt;/&lt;code&gt;v6&lt;/code&gt; (&lt;code&gt;generateText&lt;/code&gt; API rewrite), &lt;code&gt;drizzle-orm&lt;/code&gt; (parameterized template literals, not string concat).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why prompt headline, not "more context"
&lt;/h2&gt;

&lt;p&gt;The first version of this lived in &lt;code&gt;MendContext.featureSpecText&lt;/code&gt; — a freeform Markdown section the model would see somewhere in the middle of the prompt. It did approximately nothing. The model still followed tsc's quick-fix.&lt;/p&gt;

&lt;p&gt;Moving the same two sentences to the &lt;strong&gt;headline &lt;code&gt;taskDescription&lt;/code&gt;&lt;/strong&gt; — the first thing after the system instructions and before the file content — flipped the result. Same content, different position, opposite outcome.&lt;/p&gt;

&lt;p&gt;This is consistent with what we know about long-context attention falloff and how Claude in particular interprets the "task" framing: the model treats the headline as &lt;em&gt;what it's actually being asked to do&lt;/em&gt; and weights the rest of the prompt against it. "Library migration: vite-plugin-svgr" is read as "the user knows about this migration; whatever quick-fix tsc is suggesting, the migration is the reason." That single reframing overrides the gravity well of "tsc says X."&lt;/p&gt;

&lt;h2&gt;
  
  
  The same gap, with worse consequences
&lt;/h2&gt;

&lt;p&gt;The svgr case is the easy version of the failure mode: a crashed dev server is loud, immediate, and obviously broken. Move the same dynamic into security territory and the failure goes quiet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 1 — &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; as a children-type escape hatch.&lt;/strong&gt; An LLM is asked to render some user-controlled HTML in a React component. The component signature says &lt;code&gt;children: string&lt;/code&gt;, the input is an HTML string, tsc complains. The path of least resistance — and one I've seen models take — is to switch the rendering to &lt;code&gt;dangerouslySetInnerHTML={{ __html: input }}&lt;/code&gt;. The error vanishes. The XSS hole opens. Same three-character collision: React's type system &lt;em&gt;correctly&lt;/em&gt; warns that an HTML string isn't a React node; tsc enforces it; the LLM picks the dodge that makes the type-checker happy. The runtime-correct fix is to render the text as JSX (&lt;code&gt;{input}&lt;/code&gt; auto-escapes) or sanitize via DOMPurify before mounting. The type system has no way to know which of those you wanted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 2 — substituting &lt;code&gt;crypto.subtle.digest&lt;/code&gt; for a missing &lt;code&gt;bcrypt&lt;/code&gt; import.&lt;/strong&gt; A repo's &lt;code&gt;package.json&lt;/code&gt; lists &lt;code&gt;bcrypt&lt;/code&gt;; the source imports it; an LLM-generated refactor accidentally removes the import line. tsc emits &lt;code&gt;Cannot find name 'bcrypt'. Did you mean 'crypto'?&lt;/code&gt; — and the LLM dutifully takes the suggestion, switching &lt;code&gt;bcrypt.hash(password, 10)&lt;/code&gt; to &lt;code&gt;crypto.subtle.digest("SHA-256", encoder.encode(password))&lt;/code&gt;. tsc is happy. The code compiles. An unsalted, un-adaptive SHA-256 of every user password is now shipping to production. Every detail of tsc's reasoning was correct — &lt;code&gt;crypto&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; the closest in-scope identifier, and SHA-256 &lt;em&gt;does&lt;/em&gt; return a digest — but the runtime semantics are catastrophically different from a salted, adaptive-cost password hash.&lt;/p&gt;

&lt;p&gt;Both cases are the same shape as svgr, just with stakes that get someone fired instead of a broken page reload. &lt;strong&gt;tsc is a static system reasoning about types; the program is a dynamic system that has to actually work.&lt;/strong&gt; A repair that prioritizes the first over the second is the kind of fix that makes the build green and the security report red.&lt;/p&gt;

&lt;p&gt;tsfix added explicit prompt-level rules against both of these (plus &lt;code&gt;as keyof T&lt;/code&gt; to silence index-signature errors, and dropping arguments to silence TS2554). They're not magic — the model can still produce a bad fix — but they shift the prior. Across the bench, the cases that exercise these patterns went from 0/3 to 3/3 functional-and-secure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bet I'm making
&lt;/h2&gt;

&lt;p&gt;The next moat in LLM coding tools is not on the model side. The frontier-model gap has narrowed to weeks at best; everyone codes against the same three providers. The moat is on the &lt;strong&gt;structured-knowledge side&lt;/strong&gt; — the layer that injects the things a model trained on five years of mixed-version code cannot reliably know: which libraries broke, in which versions, with which migration. Library-migration registries are one form. Framework-version-aware refactoring is another. Security-pattern recognition is a third.&lt;/p&gt;

&lt;p&gt;These are unsexy databases of "this is wrong now, do that instead," extended one entry at a time by humans who hit the failure mode and submitted a fix. The first project to ship a registry serious enough to embed into Cursor / Claude Code / Continue.dev / Cline as a sub-component wins the &lt;strong&gt;post-generation correctness&lt;/strong&gt; category. That's the integration that touches every one of those tools' users, every codegen pass, every day. It compounds: every new library entry makes your tool relatively more useful versus every alternative.&lt;/p&gt;

&lt;p&gt;We've open-sourced our registry under MIT. It currently knows about &lt;code&gt;vite-plugin-svgr&lt;/code&gt; v4, &lt;code&gt;next&lt;/code&gt; v15, the Vercel AI SDK v3, and &lt;code&gt;drizzle-orm&lt;/code&gt;. Four entries is a starting line, not a finish line. The interesting thing is that adding the fifth, sixth, and hundredth entries is &lt;em&gt;exactly&lt;/em&gt; the kind of contribution this codebase is structured to receive — see &lt;a href="https://github.com/owgreen-dev/tsfix/blob/main/src/libraryMigrations.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/libraryMigrations.ts&lt;/code&gt;&lt;/a&gt;, the &lt;a href="https://github.com/owgreen-dev/tsfix/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;registry-extension guide in CONTRIBUTING.md&lt;/a&gt;, and the pinned discussion &lt;em&gt;&lt;a href="https://github.com/owgreen-dev/tsfix/discussions" rel="noopener noreferrer"&gt;"Which library should the migration registry cover next?"&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @shipispec/tsfix &lt;span class="nt"&gt;--workspace&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--llm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first run, Layer 0/1 clears the trivial errors deterministically (typos, missing imports — no LLM, no network, no cost). Layer 2 takes whatever's left, with library hints firing automatically when one of the four currently-registered packages is in your &lt;code&gt;package.json&lt;/code&gt;. You'll see a per-error tally, per-iteration token / cost numbers, and either &lt;code&gt;stopReason=fixed&lt;/code&gt; or a list of remaining errors. Layer 0/1 by itself needs no API key. Layer 2 needs &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;, &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;, or &lt;code&gt;GOOGLE_GENERATIVE_AI_API_KEY&lt;/code&gt; — your choice via &lt;code&gt;--llm-provider&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If your stack hits one of the patterns above and tsfix doesn't yet know about it, the registry-suggestion issue template is the fastest path to making sure no one else in your category hits it again.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/@shipispec/tsfix" rel="noopener noreferrer"&gt;@shipispec/tsfix&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/owgreen-dev/tsfix" rel="noopener noreferrer"&gt;github.com/owgreen-dev/tsfix&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Library-migration registry: &lt;a href="https://github.com/owgreen-dev/tsfix/blob/main/src/libraryMigrations.ts" rel="noopener noreferrer"&gt;src/libraryMigrations.ts&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;"Which library next?" discussion: &lt;a href="https://github.com/owgreen-dev/tsfix/discussions" rel="noopener noreferrer"&gt;github.com/owgreen-dev/tsfix/discussions&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
