8 releases
| new 0.4.0 | Dec 25, 2025 |
|---|---|
| 0.3.4 | Dec 9, 2025 |
| 0.3.2 | Nov 23, 2025 |
| 0.2.1 | Nov 2, 2025 |
#555 in Encoding
590KB
14K
SLoC
Tauri TypeGen
A command-line tool that automatically generates TypeScript bindings from your Tauri commands, eliminating manual frontend type creation.
Features
- 🔍 Automatic Discovery: Scans Rust source for
#[tauri::command]functions - 📝 TypeScript Generation: Creates TypeScript interfaces for command parameters and return types
- ✅ Validation Support: Optional Zod schema generation with runtime validation
- 🚀 Command Bindings: Strongly-typed frontend functions
- 📡 Event Support: Discovers and types
app.emit()events - 📞 Channel Support: Types for streaming
Channel<T>parameters - 🏷️ Serde Support: Respects
#[serde(rename)]and#[serde(rename_all)]attributes - 🎯 Type Safety: Keeps frontend and backend types in sync
- 🛠️ Build Integration: Works as standalone CLI or build dependency
Table of Contents
- Installation
- Quick Setup
- Recommended Setup
- Generated Code
- Using Generated Bindings
- TypeScript Compatibility
- API Reference
- Configuration
- Examples
- Contributing
Installation
Install globally as a CLI tool:
cargo install tauri-typegen
Or add as a build dependency to your Tauri project:
cargo add --build tauri-typegen
Quick Setup
For trying it out or one-time generation:
# Install CLI
cargo install tauri-typegen
# Generate types once
cargo tauri-typegen generate
# Use generated bindings
This generates TypeScript files in ./src/generated/ from your ./src-tauri/ code.
Recommended Setup
For integrated development workflow:
1. Install and Initialize
# Install CLI
cargo install tauri-typegen
# Initialize configuration (adds to tauri.conf.json)
cargo tauri-typegen init
# Or with custom settings
cargo tauri-typegen init --validation zod --output tauri.conf.json
This creates a configuration block in your tauri.conf.json:
{
"plugins": {
"tauri-typegen": {
"project_path": ".",
"output_path": "../src/generated",
"validation_library": "none",
"verbose": false
}
}
}
2. Add Build Hook
Add tauri-typegen as a build dependency from within your Tauri project (in the src-tauri directory):
cd src-tauri
cargo add --build tauri-typegen
cd ..
Then add to src-tauri/build.rs:
fn main() {
// Generate TypeScript bindings before build
tauri_typegen::BuildSystem::generate_at_build_time()
.expect("Failed to generate TypeScript bindings");
tauri_build::build()
}
Now types auto-generate on every Rust build:
npm run tauri dev # Types generated automatically
npm run tauri build # Types generated automatically
Generated Code
Example Rust Code
use serde::{Deserialize, Serialize};
use tauri::ipc::Channel;
#[derive(Serialize, Deserialize)]
pub struct User {
pub id: i32,
pub name: String,
pub email: String,
}
#[derive(Deserialize)]
pub struct CreateUserRequest {
pub name: String,
pub email: String,
}
#[derive(Clone, Serialize)]
pub struct ProgressUpdate {
pub percentage: f32,
pub message: String,
}
// Simple command
#[tauri::command]
pub async fn get_user(id: i32) -> Result<User, String> {
// Implementation
}
// Command with custom types
#[tauri::command]
pub async fn create_user(request: CreateUserRequest) -> Result<User, String> {
// Implementation
}
// Command with Channel for progress streaming
#[tauri::command]
pub async fn download_file(
url: String,
on_progress: Channel<ProgressUpdate>
) -> Result<String, String> {
// Send progress updates
on_progress.send(ProgressUpdate {
percentage: 50.0,
message: "Halfway done".to_string()
})?;
// Implementation
}
// Event emission
pub fn notify_user(app: &AppHandle, message: String) {
app.emit("user-notification", message).unwrap();
}
Generated Files
src/generated/
├── types.ts # TypeScript interfaces
├── commands.ts # Typed command functions
└── events.ts # Event listener functions (if events detected)
Generated types.ts:
import type { Channel } from '@tauri-apps/api/core';
export interface User {
id: number;
name: string;
email: string;
}
export interface CreateUserRequest {
name: string;
email: string;
}
export interface ProgressUpdate {
percentage: number;
message: string;
}
export interface GetUserParams {
id: number;
}
export interface CreateUserParams {
request: CreateUserRequest;
}
export interface DownloadFileParams {
url: string;
onProgress: Channel<ProgressUpdate>;
}
Generated commands.ts:
import { invoke, Channel } from '@tauri-apps/api/core';
import * as types from './types';
export async function getUser(params: types.GetUserParams): Promise<types.User> {
return invoke('get_user', params);
}
export async function createUser(params: types.CreateUserParams): Promise<types.User> {
return invoke('create_user', params);
}
export async function downloadFile(params: types.DownloadFileParams): Promise<string> {
return invoke('download_file', params);
}
Generated events.ts:
import { listen } from '@tauri-apps/api/event';
export async function onUserNotification(handler: (event: string) => void) {
return listen('user-notification', (event) => handler(event.payload as string));
}
With Zod Validation
When using --validation zod, generated commands include runtime validation:
export async function createUser(
params: types.CreateUserParams,
hooks?: CommandHooks<types.User>
): Promise<types.User> {
try {
const result = types.CreateUserParamsSchema.safeParse(params);
if (!result.success) {
hooks?.onValidationError?.(result.error);
throw result.error;
}
const data = await invoke<types.User>('create_user', result.data);
hooks?.onSuccess?.(data);
return data;
} catch (error) {
if (!(error instanceof ZodError)) {
hooks?.onInvokeError?.(error);
}
throw error;
} finally {
hooks?.onSettled?.();
}
}
Using Generated Bindings
Basic Usage
import { getUser, createUser, downloadFile } from './generated';
import { Channel } from '@tauri-apps/api/core';
// Simple command
const user = await getUser({ id: 1 });
// With custom types
const newUser = await createUser({
request: {
name: "John Doe",
email: "john@example.com"
}
});
// With Channel for streaming
const onProgress = new Channel<ProgressUpdate>();
onProgress.onmessage = (progress) => {
console.log(`${progress.percentage}%: ${progress.message}`);
};
const result = await downloadFile({
url: "https://example.com/file.zip",
onProgress
});
With Event Listeners
import { onUserNotification } from './generated';
// Listen for events
const unlisten = await onUserNotification((message) => {
console.log('Notification:', message);
});
// Stop listening
unlisten();
React Example
import React, { useState } from 'react';
import { createUser } from './generated';
import type { User } from './generated';
export function CreateUserForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const user = await createUser({
request: { name, email }
});
console.log('Created:', user);
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
<input value={email} onChange={e => setEmail(e.target.value)} />
<button type="submit">Create User</button>
</form>
);
}
With Zod Validation Hooks
import { createUser } from './generated';
import { toast } from 'sonner';
await createUser(
{ request: userData },
{
onValidationError: (err) => toast.error(err.errors[0].message),
onInvokeError: (err) => toast.error('Failed to create user'),
onSuccess: (user) => toast.success(`Created ${user.name}!`),
}
);
TypeScript Compatibility
Requirements
- TypeScript 5.0+
- Zod 4.x (when using Zod validation)
- ES2018+ target
tsconfig.json
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
Type Mappings
| Rust Type | TypeScript |
|---|---|
String, &str |
string |
i8, i16, i32, i64, i128, isize |
number |
u8, u16, u32, u64, u128, usize |
number |
f32, f64 |
number |
bool |
boolean |
() |
void |
Option<T> |
T | null |
Vec<T> |
T[] |
HashMap<K,V>, BTreeMap<K,V> |
Record<K, V> |
HashSet<T>, BTreeSet<T> |
T[] |
(T, U, V) |
[T, U, V] |
Channel<T> |
Channel<T> |
Result<T, E> |
T (errors via Promise rejection) |
Serde Attribute Support
Tauri-typegen respects serde serialization attributes to ensure generated TypeScript types match your JSON API:
Field Renaming
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct User {
#[serde(rename = "userId")]
pub user_id: i32,
pub name: String,
}
Generates:
export interface User {
userId: number; // Field renamed as specified
name: string;
}
Struct-Level Naming Convention
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiResponse {
pub user_id: i32,
pub user_name: String,
pub is_active: bool,
}
Generates:
export interface ApiResponse {
userId: number; // snake_case → camelCase
userName: string; // snake_case → camelCase
isActive: boolean; // snake_case → camelCase
}
Supported naming conventions:
camelCasePascalCasesnake_caseSCREAMING_SNAKE_CASEkebab-caseSCREAMING-KEBAB-CASE
Field Precedence
Field-level rename takes precedence over struct-level rename_all:
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct User {
pub user_id: i32, // → userId
#[serde(rename = "fullName")]
pub user_name: String, // → fullName (override)
}
Skip Fields
#[derive(Serialize, Deserialize)]
pub struct User {
pub id: i32,
#[serde(skip)]
pub internal_data: String, // Not included in TypeScript
}
Enum Support
Enums also support serde rename attributes:
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum MyEnum {
HelloWorld, // → HELLO_WORLD
ByeWorld, // → BYE_WORLD
}
Generates:
export type MyEnum = "HELLO_WORLD" | "BYE_WORLD";
// With Zod:
export const MyEnumSchema = z.enum(["HELLO_WORLD", "BYE_WORLD"]);
Variant-level rename also works:
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Status {
InProgress, // → inProgress
#[serde(rename = "not-started")]
NotStarted, // → not-started (override)
}
API Reference
CLI Commands
# Generate bindings
cargo tauri-typegen generate [OPTIONS]
Options:
-p, --project-path <PATH> Tauri source directory [default: ./src-tauri]
-o, --output-path <PATH> Output directory [default: ./src/generated]
-v, --validation <LIBRARY> Validation library: zod or none [default: none]
--verbose Verbose output
--visualize-deps Generate dependency graph
-c, --config <FILE> Config file path
# Initialize configuration
cargo tauri-typegen init [OPTIONS]
Options:
-p, --project-path <PATH> Tauri source directory [default: ./src-tauri]
-g, --generated-path <PATH> Output directory [default: ./src/generated]
-o, --output <FILE> Config file [default: tauri.conf.json]
-v, --validation <LIBRARY> Validation library [default: none]
--force Overwrite existing config
Build Script API
Add as a build dependency:
cd src-tauri
cargo add --build tauri-typegen
Then in src-tauri/build.rs:
fn main() {
// Generate TypeScript bindings
tauri_typegen::BuildSystem::generate_at_build_time()
.expect("Failed to generate TypeScript bindings");
tauri_build::build()
}
Programmatic API
use tauri_typegen::{GenerateConfig, generate_from_config};
let config = GenerateConfig {
project_path: ".".to_string(),
output_path: "../src/generated".to_string(),
validation_library: "none".to_string(),
verbose: Some(true),
};
let files = generate_from_config(&config)?;
Configuration
Standalone Config File
{
"project_path": ".",
"output_path": "../src/generated",
"validation_library": "none",
"verbose": false
}
Tauri Config Integration
In tauri.conf.json:
{
"plugins": {
"tauri-typegen": {
"project_path": ".",
"output_path": "../src/generated",
"validation_library": "zod",
"verbose": true
}
}
}
Validation Options
none(default): TypeScript types only, no runtime validationzod: Generate Zod schemas with runtime validation and hooks
Custom Type Mappings
Map external Rust types to TypeScript types for libraries like chrono, uuid, or custom types:
{
"plugins": {
"tauri-typegen": {
"project_path": ".",
"output_path": "../src/generated",
"validation_library": "zod",
"type_mappings": {
"DateTime<Utc>": "string",
"PathBuf": "string",
"Uuid": "string"
}
}
}
}
Use cases:
- External crate types:
chrono::DateTime<Utc>→string - Standard library types:
std::path::PathBuf→string - Third-party types:
uuid::Uuid→string - Custom wrapper types:
UserId→number
Example:
Rust code:
use chrono::{DateTime, Utc};
use std::path::PathBuf;
#[derive(Serialize)]
pub struct FileMetadata {
pub path: PathBuf,
pub created_at: DateTime<Utc>,
}
#[tauri::command]
pub fn get_file_info() -> FileMetadata {
// ...
}
Generated TypeScript (with mappings):
export interface FileMetadata {
path: string; // PathBuf → string
createdAt: string; // DateTime<Utc> → string
}
export async function getFileInfo(): Promise<FileMetadata> {
return invoke('get_file_info');
}
Usage in CI
When running builds in CI/CD environments, you need to generate TypeScript bindings before the frontend build step.
Why CI Needs Special Setup
The cargo tauri build command builds the frontend bundle first, before compiling Rust code. This means the build script in src-tauri/build.rs hasn't run yet, so bindings aren't generated when the frontend needs them.
Recommended CI Workflow
Install and run the CLI tool as a separate step before building:
# GitHub Actions example
- name: Install tauri-typegen
run: cargo install tauri-typegen
- name: Generate TypeScript bindings
run: cargo tauri-typegen generate
- name: Build Tauri app
run: npm run tauri build
# GitLab CI example
build:
script:
- cargo install tauri-typegen
- cargo tauri-typegen generate
- npm run tauri build
Alternative: Cache the CLI Installation
To speed up CI runs, cache the installed binary:
# GitHub Actions with caching
- name: Cache tauri-typegen
uses: actions/cache@v4
with:
path: ~/.cargo/bin/cargo-tauri-typegen
key: ${{ runner.os }}-tauri-typegen-${{ hashFiles('**/Cargo.lock') }}
- name: Install tauri-typegen
run: cargo install tauri-typegen --locked
- name: Generate bindings
run: cargo tauri-typegen generate
- name: Build
run: npm run tauri build
Examples
See the examples repository: https://github.com/thwbh/tauri-typegen-examples
Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Submit a pull request
License
This project is licensed under the MIT license.
Dependencies
~11–24MB
~285K SLoC