Use the @knighted/jsx-ts-plugin to teach the TypeScript language service how to interpret @knighted/jsx tagged templates. The plugin understands the DOM (jsx) and React (reactJsx) entrypoints and applies mode-aware diagnostics so editors surface real JSX errors inside template literals.
Important
TypeScript only loads language-service plugins inside editors (via tsserver). Running tsc or tsc --noEmit directly will not execute this plugin. To enforce the same diagnostics in CI, pair your build with a compiler transform (loader, ts-patch, etc.) or run a custom check that reuses the plugin’s transformation logic.
Requires TypeScript 5.4 or newer (matching the v0.3.x peer dependency in @knighted/jsx-ts-plugin).
npm install --save-dev @knighted/jsx-ts-pluginEnable the plugin inside the plugins section of your tsconfig.json (or jsconfig.json). Mixed DOM + React projects only need a single plugin block—the plugin routes each tag to the right mode automatically.
{
"compilerOptions": {
"plugins": [
{
"name": "@knighted/jsx-ts-plugin",
"tagModes": {
"jsx": "dom",
"reactJsx": "react"
}
}
]
}
}Tip
The tagModes entry shown above matches the built-in defaults. Keep it when you add custom tag names (for example, if you alias jsx to html), otherwise you can omit the property entirely.
tagModes: maps each tagged template function to either"dom"or"react". Defaults to{ "jsx": "dom", "reactJsx": "react" }so mixed projects work with zero config.tags/mode: legacy aliases from early releases. They continue to work buttagModesis preferred because it supports multiple identifiers at once.maxTemplatesPerFile: optional safeguard that skips files containing more tagged templates than the provided number—handy if you have giant fixtures that would otherwise slow down the language service.
Restart your editor after saving the config. From VS Code you can run TypeScript: Select TypeScript Version → Use Workspace Version to make sure the plugin loads from node_modules. The plugin repository documents every option and includes advanced examples—see knightedcodemonkey/jsx-ts-plugin for details.
jsxtemplates run in DOM mode (accepting DOM nodes, strings, iterables, etc.).reactJsxtemplates run in React mode (acceptingReactNode, hooks, and JSX component types).
Editors surface the extra diagnostics immediately because the plugin runs inside tsserver. Command-line builds still rely on whichever compiler transform or loader you configure outside this plugin.
You can override the mode per expression by dropping an inline directive immediately before the template literal:
/* @jsx-dom */ const card = jsx`<section>${value}</section>`
/* @jsx-react */ const element = jsx`<${ReactComponent} />`The directive applies only to the next tagged template, making it safe to mix DOM wrappers and React islands inside the same file without global config churn.
Directives can be line or block comments, and they work even when the tag name is custom (for example, if you alias jsx to html).
@knighted/jsx bundles a jsx-runtime entrypoint, so setting "jsxImportSource": "@knighted/jsx" gives the TypeScript compiler everything it needs for TSX files.
Remember that DOM-mode helpers return JsxRenderable (real DOM nodes, strings, iterables, etc.) while React-mode helpers return ReactElement. When sharing utilities between the two ecosystems, let inference pick the right return type and only cast when you truly need DOM-only APIs.
The runtime exports the JsxRenderable helper so DOM templates never have to cast through ReactNode. Use it when you surface values from pure functions or external libraries:
import type { JsxRenderable } from '@knighted/jsx'
const asRenderable = (input: unknown): JsxRenderable => {
if (input instanceof Node) return input
return String(input ?? '')
}
const view = jsx`<span>${asRenderable(payload)}</span>`React mode continues to rely on ReactNode, so projects that import both helpers can keep using the standard React types.
When you build DOM-only helpers (like badges rendered into Lit components), type them with JsxRenderable so you never have to cast to ReactNode:
import { jsx } from '@knighted/jsx'
import type { JsxRenderable } from '@knighted/jsx'
type DomBadgeProps = { label: JsxRenderable }
export const DomBadge = ({ label }: DomBadgeProps): HTMLElement => {
let clicks = 0
const counterText = jsx`<span>Clicked ${clicks} times</span>` as HTMLSpanElement
return jsx`
<article class="dom-badge">
<header>
<h2>Lit + DOM with jsx</h2>
<p data-kind="react">${label}</p>
</header>
<button
type="button"
data-kind="dom-counter"
onClick=${() => {
clicks += 1
counterText.textContent = `Clicked ${clicks} times`
}}
>
${counterText}
</button>
</article>
` as HTMLDivElement
}Here label stays fully typed as a DOM-friendly value, and the component returns an HTMLElement, so nothing needs to be widened to ReactNode.
- Install
@knighted/jsx-ts-pluginas a dev dependency. - Add a single plugin block in
tsconfig.json(as shown above) or extend it with additionaltagModesfor custom tags. - Restart your editor and point VS Code at the workspace TypeScript version so the plugin loads.
- Pair your CI/build step with the loader or compiler transform you already use for
@knighted/jsxtemplates—tsc --noEmitalone will not load the language-service plugin.
Following the checklist keeps DOM and React templates aligned across the entire toolchain—no ReactNode casts, no mismatched compiler results, and no duplicate plugin entries.