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
- Tauri Integration
- Generated Output
- Namespace Guide
- Error Guide
- Zod Guide
- Typed Events
- Feature Guide
- Benchmark Results
- Roadmap
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.
| Benchmark | Median | P95 | Notes |
|---|---|---|---|
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:
For full benchmark methodology and output formats, see 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.
| Capability | Tyzen | tauri-specta | ts-rs |
|---|---|---|---|
| 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-spectaspecta: https://github.com/specta-rs/spectats-rs: https://github.com/Aleph-Alpha/ts-rs
Quick Start
1. Add Dependencies
For core type generation:
For Tauri integration (Optional):
2. Define Your Types (Rust)
Tyzen uses simple attributes to mark structs, enums, and functions.
use ;
// 1. Convert any Rust struct/enum to TS
// 2. Define a typed Event
3. Setup the Generator
Run generation before frontend type-check/build so bindings.ts always matches Rust.
- Core (non-Tauri):
- Tauri (recommended default):
- Tauri with config (for example
Option<T> -> field?: T):
- Non-Tauri workflow (
package.json):
// src/bin/gen_bindings.rs
If backend is in another workspace package:
- Tyzen rewrites the target file only when generated content actually changes.
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.
// Marks for TS generation & auto-registers with Tauri
2. Setup Generator & Handler
Frontend Usage (TypeScript/React)
Tyzen creates a clean, intuitive API for your frontend.
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:
// 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.
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, ...)
- global type definitions (
Command naming inside a namespace:
- Default strategy is
Prefix. - With namespace
Task,task_get_allbecomesTask.getAll. Postfixstrategy is also supported, soget_all_taskbecomesTask.getAll.#[tyzen(rename = "...")]on a command wins over auto strip logic.
module_ns!;
Generated frontend shape:
import { Task } from './bindings';
const res = await Task.getAll();
If your team prefers postfix names (get_all_task style), set naming strategy:
generate_with_config?;
Per-command explicit rename (works for both core and tauri command attrs):
Error Handling Pattern
Keep backend errors typed, then map them to user-facing messages in frontend.
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);
}
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:
Generated output example (bindings.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>;
Typed Events
When you derive tyzen::Event, Tyzen adds a helper .emit(&handle) method to your struct:
let event = WelcomeEvent ;
event.emit.ok; // Correctly types the payload for the frontend
Roadmap & Status
| Feature | Importance | Notes |
|---|---|---|
| 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 forTypeandEvent.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/testsortyzen-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.