React and JSX

TypeRB can emit ordinary TSX for existing React tools. This browser integration is an initial alpha feature and may evolve before TypeRB reaches 1.0.

Run a first component

Create a browser TypeScript project:

trb init --mode typescript --runtime browser react-app
cd react-app

Create main.trb. Import the explicit React platform package and use a record for component props:

import { ReactNode, mount } from trb/platform/typescript/react

record GreetingProps
	name: String
end

def Greeting(props: GreetingProps): ReactNode
	return <p>Hello, {props.name}</p>
end

def App(): ReactNode
	return <main><Greeting name="TypeRB" /></main>
end

def main()
	mount(<App />, "root")
end

Create index.html for Vite:

<!doctype html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>TypeRB React</title>
	</head>
	<body>
		<div id="root"></div>
		<script type="module" src="/build/main.tsx"></script>
	</body>
</html>

Add Vite, install the generated React dependencies, and build the TypeRB source:

trb add --native --dev vite
trb install
trb fmt
trb check
trb build
npm exec vite

Open the URL printed by Vite, then press Ctrl-C to stop it. The default TypeScript browser project uses npm and therefore requires Node.js. Set typescript.packageManager to "bun" if the project uses Bun instead.

The compiler checks required, unknown, and mistyped props, including components imported from another TypeRB module. Files containing JSX are generated as .tsx; other TypeScript modules remain .ts.

Component state

Component-local state uses a typed wrapper around React useState:

import { MouseEvent, ReactNode, use_state } from trb/platform/typescript/react

def Counter(): ReactNode
	count := use_state(0)
	increment := fn(_event: MouseEvent)
		count.set(count.value + 1)
	end
	return <button onClick={increment}>Count: {count.value}</button>
end

use_state(initial) infers ReactState<T> from its initial value. value and set(value) retain that type through checking and completion, while generated TSX uses React's ordinary useState. The call follows the Rules of Hooks and therefore stays at the top level of a component or custom hook.

Native React packages

TypeScript projects can use representable named exports from npm packages without application-owned TypeRB declarations:

trb add --native react-spinners ^0.17.0
trb install
import { ReactNode } from trb/platform/typescript/react
import { ClipLoader } from "react-spinners"

def Loading(): ReactNode
	return <ClipLoader color="#4f46e5" loading size={24} />
end

trb install reads the installed package's .d.ts declarations, including dependency subpaths imported by project source, and writes an ignored .trb/native-types.json index. Builds and editor completion read that index without starting TypeScript. Index generation currently requires the supported TypeScript 6.x toolchain. Runtime output imports the original npm package unchanged. The compiler-owned cache pools repeated types, fields, and diagnostics, so component suites with shared or inherited DOM props store that metadata once instead of once per component. Run trb install to regenerate the cache after a TypeRB upgrade changes its internal format.

Automatic .d.ts indexing supports ordinary functions and React components whose signatures can be represented safely by TypeRB types. Unsupported overloads, conditional types, any, and individual complex props produce a diagnostic instead of becoming Any. A TypeRB package may provide declarative generic functions, classes with explicit instance and class members, records, and discriminated result aliases for a complex native package. Application code still imports the original npm package and writes explicit TypeRB type arguments; it does not maintain a local signature file. See the package guide.

Event attributes use purpose-specific React types instead of one untyped event:

import { ChangeEvent, FormEvent, ReactNode } from trb/platform/typescript/react

def TodoForm(): ReactNode
	on_change := fn(event: ChangeEvent)
		puts(event.currentTarget.value)
	end
	on_submit := fn(event: FormEvent)
		event.preventDefault()
	end
	return <form onSubmit={on_submit}>
		<input onChange={on_change} />
	</form>
end

The initial event set includes MouseEvent, ChangeEvent, FormEvent, and KeyboardEvent. React and DOM spellings such as onClick, preventDefault, and currentTarget are preserved without automatic name conversion.

Generated TSX and managed React package dependencies are compatible with the normal React and Vite ecosystem. Use the separate browser HTTP client for typed requests; suspension remains a TypeScript backend concern and does not add async syntax to TypeRB.