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": ".",
"output_path": "../src/generated",
"validation_library": "none",
"verbose": false
}
}
}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 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: "[email protected]"
}
});
// 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 |
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) |
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 configAdd as a build dependency:
cd src-tauri
cargo add --build tauri-typegenThen 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()
}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)?;{
"project_path": ".",
"output_path": "../src/generated",
"validation_library": "none",
"verbose": false
}In tauri.conf.json:
{
"plugins": {
"tauri-typegen": {
"project_path": ".",
"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
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');
}When running builds in CI/CD environments, you need to generate TypeScript bindings before the frontend build step.
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.
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 buildTo 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 buildSee 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.