TSRX | TypeScript Language Extension for Declarative UI

5 min read Original article ↗

TSRX is a TypeScript language extension for building declarative UIs in an agentic era.

TSRX (TypeScript Render Extensions) is a way to write UI component code that stays readable and co-located. Structure, styling, and control flow live together, and the result stays fully backwards compatible with TypeScript.

 1 export component Greeting({ name }: { name?: string }) {
 2   <div class="card">
 3     if (name) {
 4       <p>{`Hello, ${name}`}</p>
 5     } else {
 6       <p>{'Hello, stranger'}</p>
 7     }
 8   </div>
 9
10   <style>
11     .card { padding: 1rem; }
12   </style>
13 }

You can think of TSRX as a spiritual successor to JSX — the same mental model of embedding UI directly inside TypeScript, but with its own flavor. Control flow, scoped styles, and locals sit in the template as first-class syntax instead of being squeezed through expression slots, and the language stays aware of them through to the compiled output.

TSRX is framework-agnostic and interoperable. Today it compiles to React, Preact, Ripple, and Solid, with room for more targets over time. You can import .tsrx modules from JS, TS, and TSX files and it just works.

Looking for the formal grammar and AST shape? Read the specification →

Benefits of TSRX

Co-location helps engineers and AI systems alike.

When structure, control flow, styling, and component shape live in the same file, the source reads closer to the interface it describes. That cuts down on ternaries, map chains, and render helpers. It also makes refactors and onboarding less brittle.

 1 component UserList({ users, showBio }: Props) {
 2   for (const user of users) {
 3     const initials = user.name.slice(0, 2).toUpperCase();
 4     const role = user.admin ? 'Admin' : 'Member';
 5
 6     <div class="user-row">
 7       <span class="avatar">{initials}</span>
 8       <strong>{user.name}</strong>
 9       <span class="badge">{role}</span>
10
11       if (showBio && user.bio) {
12         const short_bio = user.bio.slice(0, 140);
13         const report = () => console.log(`viewed ${user.name}`);
14
15         <p class="bio">{short_bio}</p>
16         <button onClick={report}>{'Viewed'}</button>
17       }
18     </div>
19   }
20 }

Code is increasingly written, reviewed, and transformed with machine assistance. When UI structure is more explicit, editors, compilers, and code generation tools have a better surface to work with without changing the framework semantics underneath.

This is consistent with findings that language models attend unevenly to long contexts, performing best when relevant information sits close together rather than scattered across a window.

Lost in the Middle (Liu et al., TACL 2024) →

Ergonomics

Better ergonomics for today's frameworks.

TSRX takes care of the awkward parts each framework asks you to work around. Patterns that are natural to write — but fragile or illegal in the target runtime — get rewritten at compile time into the exact shape React, Preact, Solid, or Ripple expects. The source stays readable for humans and predictable for language models; the compiler handles the footguns.

React hooks can be called conditionally. The compiler lifts the conditional branch into its own component so the rules of hooks are satisfied by construction — no more manually splitting components just to gate a useUser(...) call behind an if.

 1 component Profile({ userId }: { userId: string | null }) {
 2   if (userId) {
 3     const user = useUser(userId);
 4     <h1>{user.name}</h1>
 5   } else {
 6     <a href="/login">{'Sign in'}</a>
 7   }
 8 }

Solid props can be destructured eagerly. The &{ ... } pattern compiles to lazy getters, so reads stay reactive without forcing props.count everywhere and without losing type inference on the destructured names.

 1 component Counter(&{ count, label }: Props) {
 2   <section>
 3     <h2>{label}</h2>
 4     <p>{`Count: ${count}`}</p>
 5   </section>
 6 }

Locals can live next to the JSX that uses them. Variables declared mid-template stay scoped to their surrounding block, so derived values sit beside the markup they feed instead of piling up at the top of the component. Concerns stay together; readers (and LLMs) do less bookkeeping to follow the data flow.

 1 component Cart({ items }: { items: Item[] }) {
 2   <div>
 3     <h2>{'Your cart'}</h2>
 4
 5     const subtotal = items.reduce((sum, item) => sum + item.price, 0);
 6     const discount = subtotal > 100 ? 0.1 : 0;
 7
 8     <p>{`Subtotal: ${subtotal}`}</p>
 9     <p>{`Save: ${(subtotal * discount).toFixed(2)}`}</p>
10   </div>
11 }

Tooling and adoption

Rich integration with existing tooling.

TSRX ships with a language server for editor diagnostics, navigation, and completion. Prettier and ESLint plugins keep formatting and linting consistent inside existing repositories, so the language can be adopted seriously instead of treated like a compiler experiment. Plus support for many IDEs and editors including VS Code, Zed, Neovim, IntelliJ and Sublime.

It interoperates with existing TypeScript and TSX codebases rather than demanding a full rewrite. Teams can adopt it where ergonomics matter most while keeping the surrounding code familiar.

Smart compilation

TSRX compiles to multiple framework runtime outputs.

The TSRX compiler parses component source into an AST and hands it off to framework-specific plugins for code generation. Plugins such as @tsrx/react, @tsrx/preact, @tsrx/ripple, and @tsrx/solid transform the same AST into idiomatic output for their target runtimes, including scoped CSS. New targets can be added as standalone compiler plugins without changing the language itself.

Try it in the playground →