A command-line tool that automatically generates TypeScript bindings from your Tauri commands, eliminating manual frontend type creation.
- 🔍 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
- Installation
- Quick Setup
- Recommended Setup
- Generated Code
- Using Generated Bindings
- TypeScript Compatibility
- API Reference
- Configuration
- Examples
- Contributing
Install globally as a CLI tool:
cargo install tauri-typegenOr add as a build dependency to your Tauri project:
cargo add --build tauri-typegenFor trying it out or one-time generation:
# Install CLI
cargo install tauri-typegen
# Generate types once
cargo tauri-typegen generate
# Use generated bindingsThis generates TypeScript files in ./src/generated/ from your ./src-tauri/ code.
For integrated development workflow:
# 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.jsonThis creates a configuration block in your tauri.conf.json:
{
"plugins": {
"tauri-typegen": {
"project_path": "./src-tauri",
"output_path": "./src/generated",
"validation_library": "none",
"verbose": false
}
}
}Add to src-tauri/build.rs:
fn main() {
// Generate TypeScript bindings before build
tauri_typegen::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 automaticallyuse 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();
}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));
}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?.();
}
}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
});import { onUserNotification } from './generated';
// Listen for events
const unlisten = await onUserNotification((message) => {
console.log('Notification:', message);
});
// Stop listening
unlisten();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>
);
}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 5.0+
- Zod 4.x (when using Zod validation)
- ES2018+ target
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}| Rust Type | TypeScript |
|---|---|
String, &str |
string |
i32, f64, etc. |
number |
bool |
boolean |
Option<T> |
T | null |
Vec<T> |
T[] |
HashMap<K,V> |
Map<K,V> |
(T,U) |
[T,U] |
Channel<T> |
Channel<T> |
Result<T,E> |
T (errors via Promise rejection) |
Tauri-typegen respects serde serialization attributes to ensure generated TypeScript types match your JSON API:
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;
}#[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-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)
}#[derive(Serialize, Deserialize)]
pub struct User {
pub id: i32,
#[serde(skip)]
pub internal_data: String, // Not included in TypeScript
}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)
}# 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// In src-tauri/build.rs
fn main() {
tauri_typegen::generate_at_build_time()
.expect("Failed to generate TypeScript bindings");
tauri_build::build()
}use tauri_typegen::{GenerateConfig, generate_from_config};
let config = GenerateConfig {
project_path: "./src-tauri".to_string(),
output_path: "./src/generated".to_string(),
validation_library: "none".to_string(),
verbose: Some(true),
};
let files = generate_from_config(&config)?;{
"project_path": "./src-tauri",
"output_path": "./src/generated",
"validation_library": "none",
"verbose": false
}In tauri.conf.json:
{
"plugins": {
"tauri-typegen": {
"project_path": "./src-tauri",
"output_path": "./src/generated",
"validation_library": "zod",
"verbose": true
}
}
}none(default): TypeScript types only, no runtime validationzod: Generate Zod schemas with runtime validation and hooks
See the examples repository: https://github.com/thwbh/tauri-typegen-examples
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Submit a pull request
This project is licensed under the MIT license.