<div align="center">
<img height="150" src="assets/logo/logo.svg" alt="Tyzen Logo" />
<h1>Tyzen</h1>
<p><b>Type-safe Rust ↔ TypeScript generation</b></p>
<a href="https://crates.io/crates/tyzen"><img src="https://img.shields.io/crates/v/tyzen.svg?style=flat-square" alt="crates.io version" /></a>
<a href="https://crates.io/crates/tyzen"><img src="https://img.shields.io/crates/d/tyzen.svg?style=flat-square" alt="downloads" /></a>
<a href="https://docs.rs/tyzen"><img src="https://img.shields.io/docsrs/tyzen?style=flat-square" alt="docs.rs" /></a>
<a href="https://crates.io/crates/tyzen"><img src="https://img.shields.io/crates/l/tyzen.svg?style=flat-square" alt="license" /></a>
</div>
Tyzen is a high-performance developer tool that bridges the gap between **Rust** and **TypeScript**. Its primary goal is to provide a seamless, type-safe development experience (DX) by automating the synchronization of data structures and API definitions.
While Tyzen was built with **Tauri** in mind, its core is a generic, lightning-fast type generator that can be used in any Rust/TS project.
## Quick Links
- [Quick Start](#quick-start)
- [Tauri Integration](#tauri-integration)
- [Generated Output](#example-generated-output-bindingsts)
- [Namespace Guide](#namespace-pattern)
- [Error Guide](#error-handling-pattern)
- [Zod Guide](#zod-schema-generation)
- [Typed Events](#typed-events)
- [Feature Guide](#-feature-guide)
- [Benchmark Results](#benchmark-results)
- [Roadmap](#-roadmap--status)
## Why Tyzen?
- **Performance**: Generates complex TypeScript bindings in milliseconds.
- **Developer Experience (DX)**: Zero-config command registration and strongly-typed events.
- **Transparency**: Keeps your code clear and explicit.
- **Tauri Integration**: First-class support for Tauri commands and events with automatic registration.
## Benchmark Results
Latest committed benchmark artifacts are in [`tyzen/bench/results`](./tyzen/bench/results).
| `e2e-generate` | `0.132 ms` | `0.233 ms` | Real `tyzen::generate(...)` flow, small realistic fixture (`2824` bytes output). |
| `e2e-heavy-generate` | `5.993 ms` | `7.193 ms` | Real `tyzen::generate(...)` flow, heavy fixture (`542292` bytes output). |
| `cold-process-generate` | `293.804 ms` | `961.796 ms` | Full cold process startup + generate (includes process bootstrap variance). |
| `codegen-large` | `3.404 ms` | `3.939 ms` | Synthetic renderer throughput (`1000` types, `24` fields/type). |
Reproduce locally:
```bash
cargo bench -p tyzen --bench codegen
cargo bench -p tyzen --bench e2e
cargo bench -p tyzen --bench e2e_heavy
./tyzen/bench/scripts/run_cold_generate.sh 6 1
```
For full benchmark methodology and output formats, see [`tyzen/bench/README.md`](./tyzen/bench/README.md).
---
## Comparison: Tyzen vs tauri-specta vs ts-rs
Tyzen already covers the core job people usually reach for `ts-rs` for: deriving TypeScript from Rust structs/enums with Serde-aware naming, optional fields, flattening, tagged enums, generics, and deterministic output.
The bigger difference is scope. `ts-rs` is a mature type-export crate. `tauri-specta` is part of the Specta stack, where `specta` provides type introspection/export, `rspc` provides end-to-end typed APIs, and `tauri-specta` provides typed Tauri commands. Tyzen is narrower than the full Specta ecosystem, but deeper on the Tauri DX it owns: generated command wrappers, events, namespaces, binary hydration, result/error handling, constants, and a predictable single-file SDK-style output.
Tyzen's killer DX feature for Tauri teams is that `.invoke_handler(tyzen_tauri::handler!())` auto-collects all macro-registered handlers, while Specta-based setups typically require explicit manual handler lists, which is easier to drift and gives weaker command-surface autocomplete ergonomics.
| Primary focus | Rust -> TS type generation + Tauri command/event DX | Typed Tauri commands/events on top of Specta | Rust -> TS type generation |
| Tauri command wrappers | ✅ Built-in via `tyzen-tauri` | ✅ Built-in | ❌ Not primary scope |
| Event typing for Tauri | ✅ Built-in event helpers | ✅ Supported | ❌ Not primary scope |
| Namespace-oriented SDK output | ✅ First-class (`module_ns!`, rename strategy) | ⚠️ Depends on your Specta export style | ⚠️ Manual conventions |
| Serde-oriented type mapping | ✅ Core goal | ✅ Via Specta + serde integrations | ✅ Core goal |
| Export granularity | Single generated bindings file by default | Exporter-driven | Per-type export and dependency export |
| Advanced per-field overrides | Partial (`rename`, `skip`, `flatten`, `optional`, `binary`) | Specta-driven | Mature (`type`, `as`, `inline`, `skip`, optional variants) |
| Generic edge cases | Common generics | Strong type graph model | Mature escape hatches (`concrete`, `bound`) |
| External crate coverage | Growing (`uuid`, `chrono`, `anyhow` features) | Broad feature ecosystem | Broad Rust type coverage |
| Ecosystem coupling | Low to medium (Tyzen crates) | Medium to high (Specta/rspc/tauri-specta alignment) | Low |
| Good default when... | You want typed Tauri IPC + predictable generated API surface | You already use Specta/rspc or want a shared type graph across tools | You only need mature standalone TS type exports |
### Decision Guide
- Choose **Tyzen** when you want type export, Tauri command wiring, event helpers, and frontend ergonomics in one explicit generator flow.
- Choose **tauri-specta** when you already use Specta/rspc, want to share one type graph across that stack, or prefer Specta's exporter model.
- Choose **ts-rs** when your use case is pure model syncing and you need its mature escape hatches for custom type overrides, inlining, concrete generics, or per-type export layout.
Reference links:
- `tauri-specta`: https://github.com/specta-rs/tauri-specta
- `specta`: https://github.com/specta-rs/specta
- `ts-rs`: https://github.com/Aleph-Alpha/ts-rs
---
<a id="quick-start"></a>
## Quick Start
### 1. Add Dependencies
For core type generation:
```bash
cargo add tyzen
cargo add serde --features derive
```
For **Tauri** integration (Optional):
```bash
cargo add tyzen-tauri
```
### 2. Define Your Types (Rust)
Tyzen uses simple attributes to mark structs, enums, and functions.
```rust
use serde::{Serialize, Deserialize};
// 1. Convert any Rust struct/enum to TS
#[derive(tyzen::Type, Serialize, Deserialize)]
pub struct User {
pub id: u32,
pub name: String,
}
// 2. Define a typed Event
#[derive(tyzen::Type, tyzen::Event, Serialize)]
pub struct WelcomeEvent {
pub message: String,
}
```
### 3. Setup the Generator
Run generation before frontend type-check/build so `bindings.ts` always matches Rust.
- Core (non-Tauri):
```rust
fn main() {
tyzen::generate("../src/bindings.ts").expect("failed to generate bindings");
}
```
- Tauri (recommended default):
```rust
fn main() {
#[cfg(debug_assertions)]
tyzen_tauri::generate("../src/bindings.ts").expect("failed to generate bindings");
tauri::Builder::default()
.invoke_handler(tyzen_tauri::handler!())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
```
- Tauri with config (for example `Option<T> -> field?: T`):
```rust
fn main() {
#[cfg(debug_assertions)]
tyzen_tauri::generate_with_config(
"../src/bindings.ts",
tyzen::GeneratorConfig {
option_fields_as_optional: true,
..Default::default()
},
)
.expect("failed to generate bindings");
tauri::Builder::default()
.invoke_handler(tyzen_tauri::handler!())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
```
- Non-Tauri workflow (`package.json`):
```rust
// src/bin/gen_bindings.rs
fn main() {
tyzen::generate("../frontend/src/bindings.ts").expect("failed to generate bindings");
}
```
```json
{
"scripts": {
"gen:bindings": "cargo run --bin gen_bindings",
"build": "pnpm gen:bindings && tsc -b && vite build"
}
}
```
If backend is in another workspace package:
```bash
cargo run -p backend --bin gen_bindings
```
- Tyzen rewrites the target file only when generated content actually changes.
---
<a id="tauri-integration"></a>
## Tauri Integration
For Tauri projects, `tyzen-tauri` provides a wrapper that automates command registration and event handling.
### 1. Define Tauri Commands
No stacked macros needed! The `#[tyzen_tauri::command]` macro automatically expands and registers your commands with Tauri under the hood.
```rust
#[tyzen_tauri::command] // Marks for TS generation & auto-registers with Tauri
pub fn create_user(name: String) -> Result<User, String> {
Ok(User { id: 1, name })
}
```
### 2. Setup Generator & Handler
```rust
fn main() {
// 1. Generate TS bindings with Tauri support
#[cfg(debug_assertions)]
tyzen_tauri::generate("../src/bindings.ts").expect("failed to generate bindings");
// 2. Setup Tauri with auto-registration
tauri::Builder::default()
.invoke_handler(tyzen_tauri::handler!()) // Auto-registers all #[tyzen_tauri::command]
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
```
---
## Frontend Usage (TypeScript/React)
Tyzen creates a clean, intuitive API for your frontend.
```tsx
import { useEffect, useState } from 'react';
import { commands, events } from './bindings';
function App() {
const [status, setStatus] = useState('Ready');
useEffect(() => {
// Listen once when component mounts
const unlisten = events.welcome.listen(payload => {
setStatus(`Message: ${payload.message}`);
});
return () => {
unlisten.then(f => f());
};
}, []);
const handleCreateUser = async () => {
// Command calls can be made anywhere (button click, form submit, effect, etc.)
const res = await commands.createUser('rzust');
if (res.status === 'ok') console.log('Success:', res.data);
};
return (
<>
<button onClick={handleCreateUser}>Create user</button>
<h1>{status}</h1>
</>
);
}
```
### Example Generated Output (`bindings.ts`)
Tyzen output is plain TypeScript. This is what your frontend imports directly:
```ts
// auto-generated by tyzen, do not edit
export type User = { id: number; name: string };
export type Result<T, E = string> =
| { status: 'ok'; data: T }
| { status: 'error'; error: E };
export const commands = {
createUser: (name: string) => __invoke<Result<User>>('create_user', { name }),
};
export const events = {
onWelcome: (cb: (payload: { message: string }) => void) =>
__listen('welcome', cb),
};
```
What this gives you:
- Rust snake_case command names become camelCase frontend functions.
- Return values are strongly typed (`Result<User>` in this example).
- Event payloads are typed at subscription sites.
---
## Feature Guide
### Standard Type Conversion
Use `#[derive(tyzen::Type)]` on any Rust type. It supports primitives, `Vec`, `Option`, `HashMap`, and even complex **Generics**.
### Tauri Commands
The `#[tyzen_tauri::command]` macro is the only attribute you need to declare a type-safe Tauri command. It automatically handles code generation and expands `#[tauri::command]` so there is zero boilerplate or stacked-macro footguns.
<a id="namespace-pattern"></a>
## Namespace Pattern
Use namespaces when you want an SDK-like frontend API (`Task.getAll()`) instead of one flat command list.
How namespace resolution works:
- `tyzen::module_ns!("Task")` sets a default namespace for that module tree.
- `#[tyzen(ns = "...")]` on a specific type/command/event overrides module default.
- Output includes both:
- global type definitions (`TaskItem`, `User`, ...)
- namespaced action objects (`Task`, `Auth`, ...)
Command naming inside a namespace:
- Default strategy is `Prefix`.
- With namespace `Task`, `task_get_all` becomes `Task.getAll`.
- `Postfix` strategy is also supported, so `get_all_task` becomes `Task.getAll`.
- `#[tyzen(rename = "...")]` on a command wins over auto strip logic.
```rust
tyzen::module_ns!("Task");
#[derive(tyzen::Type)]
pub struct TaskItem {
pub id: u64,
pub title: String,
}
#[tyzen::command]
pub fn task_get_all() -> Vec<TaskItem> {
vec![]
}
```
Generated frontend shape:
```ts
import { Task } from './bindings';
const res = await Task.getAll();
```
If your team prefers postfix names (`get_all_task` style), set naming strategy:
```rust
tyzen_tauri::generate_with_config(
"../src/bindings.ts",
tyzen::GeneratorConfig {
naming_strategy: tyzen::NamingStrategy::Postfix,
..Default::default()
},
)?;
```
Per-command explicit rename (works for both core and tauri command attrs):
```rust
#[tyzen::command(ns = "Task", rename = "getAll")]
pub fn task_get_all() -> Vec<TaskItem> {
vec![]
}
```
<a id="error-handling-pattern"></a>
## Error Handling Pattern
Keep backend errors typed, then map them to user-facing messages in frontend.
```ts
import { parseError } from './bindings/helpers';
import { ProjectErrorMeta } from './bindings';
const res = await commands.projectCreate(payload);
if (res.status === 'error') {
const uiError = parseError(res.error, ProjectErrorMeta);
console.error(uiError.code, uiError.message);
}
```
<a id="zod-schema-generation"></a>
## Zod Schema Generation
What is already supported:
- Opt-in schema generation with `#[tyzen(schema)]`.
- Struct schema generation (`z.object(...)`) for common field types.
- Unit enums to `z.enum([...])`.
- Basic validation sync (`min/max/regex`, numeric bounds).
- Generated inferred schema aliases (`z.infer<typeof ...>`).
What will be added next:
- Tagged/untagged payload enum schemas (discriminated union style support).
- Better explicit fallback visibility when generation must use `z.any()`.
- Broader external type mapping and deeper nested schema coverage.
Rust example:
```rust
#[derive(tyzen::Type)]
#[tyzen(schema)]
pub struct CreateProjectDto {
#[validate(length(min = 2, max = 60))]
pub title: String,
pub priority: Priority,
pub target_date: Option<String>,
}
#[derive(tyzen::Type)]
pub enum Priority {
Low,
Medium,
High,
}
```
Generated output example (`bindings.ts`):
```ts
import { z } from 'zod';
export type CreateProjectDto = {
title: string;
priority: Priority;
target_date: string | null;
};
export const prioritySchema = z.enum(['Low', 'Medium', 'High']);
export type PrioritySchema = z.infer<typeof prioritySchema>;
export const createProjectDtoSchema = z.object({
title: z.string().min(2).max(60),
priority: z.enum(['Low', 'Medium', 'High']),
target_date: z.union([z.string(), z.date()]).nullable().optional(),
});
export type CreateProjectDtoSchema = z.infer<typeof createProjectDtoSchema>;
```
<a id="typed-events"></a>
## Typed Events
When you derive `tyzen::Event`, Tyzen adds a helper `.emit(&handle)` method to your struct:
```rust
let event = WelcomeEvent { message: "Hi!".into() };
event.emit(&handle).ok(); // Correctly types the payload for the frontend
```
---
## Roadmap & Status
| **Full Serde Parity** | Implemented | `flatten`, `alias`, `default`, and `rename_all` support. Requires inter-type metadata. |
| **Binary Data** | Implemented | Automatically maps `Vec<u8>` or fields marked with `#[tyzen(binary)]` to `Uint8Array` with transparent hydration. |
| **Result & Error** | Implemented | Deep support for custom Rust error types and enum variant metadata blocks in frontend. |
| **Constant Export** | Implemented | Sync `pub const` logic values from Rust to TS. |
| **Namespaces** | Implemented | Organize types and commands into logical Models (SDK style). |
| **Zod Support** | Partial | Generate Zod schemas alongside types for runtime frontend validation. |
| **Mock Client** | Will implement | Generate mock JS/TS clients for testing/UI prototyping without the backend. |
| **Doc Propagation** | | Transform Rust doc comments (`///`) into TSDoc (`/** ... */`). |
---
## Packages
- `tyzen`: The core engine for type conversion.
- `tyzen-macro`: Procedural macros for `Type` and `Event`.
- `tyzen-tauri`: Specialized integration for Tauri (commands, event emitters, and TS glue code).
## Contributing
You can contribute even if this is your first OSS project:
- Open an issue describing bug/feature before large changes.
- Fork, create a branch, and open a PR with a focused diff.
- Add or update tests for behavior changes in `tyzen/tests` or `tyzen-tauri/tests`.
- Include before/after snippets for generated TypeScript when relevant.
Small first contributions that help a lot:
- Add coverage for edge-case Serde mappings.
- Improve docs/examples around command/event patterns.
- Add mappings for common external Rust types.
## License
Distributed under the MIT / Apache-2.0 License.