A powerful CLI tool to automatically generate TypeScript bindings from your Rust Tauri commands, structs, and enums.
tauri-ts-generator scans your Rust source code, parses #[tauri::command] macros and data structures, and generates type-safe TypeScript interfaces and invocation functions. This eliminates manual typing boilerplate and ensures your frontend and backend are always in sync.
- Automated Scanning: Recursively scans your
src-tauridirectory for commands and types. - Type Safety: Generates exact TypeScript definitions for Rust structs, enums, and type aliases.
- Serde Support:
- Field and variant names in generated TypeScript match exactly what serde emits in JSON at runtime.
- Respects
#[serde(rename = "...")]on fields and variants. - Handles
#[serde(rename_all = "...")]for enums and structs (lowercase,UPPERCASE,camelCase,PascalCase,snake_case,SCREAMING_SNAKE_CASE,kebab-case,SCREAMING-KEBAB-CASE). - Supports
#[serde(tag = "...")],#[serde(content = "...")], and#[serde(untagged)]enum representations. - Supports
#[serde(flatten)]to generate TypeScript intersection types. - Fields with
#[serde(skip)]are excluded from TypeScript output. - Support for
#[ts(optional)]attribute onOptionfields to generateprop?: Tinstead ofT | null. - Provides
#[derive(tauri_ts_generator::TS)]to register thetsattribute namespace.
- Smart Type Mapping:
- Maps common Rust types (
String,Vec,Option,Result) to TypeScript equivalents. - Handles external crate types like
chrono::DateTime,uuid::Uuid,url::Url, andrust_decimal::Decimal.
- Maps common Rust types (
- Async Handling: Correctly generates
Promise<T>for async commands. - Tauri Integration:
- Automatically imports
invokefrom@tauri-apps/api/core. - Supports
#[tauri::command(rename_all = "...")]to control argument casing (e.g.snake_case). - Supports
tauri::ipc::Channel<T>command arguments: typed asChannel<T>in the generated signature (with theChannelimport added when needed), plus an exported<Command><Arg>ChannelTypealias for each channel payload in the types file (sincev2.1.0).
- Automatically imports
- Macro Support: Optional integration with
cargo-expandresolves both types and#[tauri::command]functions generated by macros. Amacro_rules!that stamps out N typed commands (or a proc-macro likeprogenitor) is invisible to a raw-source parser; withuse_cargo_expand = truethose commands appear in the generated TypeScript automatically (sincev2.0.3). - Conflict Resolution: Detects and handles naming conflicts or ambiguous imports.
cargo install tauri-ts-generatorOr run directly from source:
cargo run --release -- generate-
Initialize Configuration (Run in your Tauri project root):
tauri-ts-generator init
This creates a
tauri-codegen.tomlfile. -
Generate Bindings:
tauri-ts-generator generate
The CLI is the common path, but the crate is also a library: the whole CLI is a thin
wrapper over two public types, Config and Pipeline. That makes it easy to regenerate
bindings on every build instead of by hand.
Add it as a build dependency:
# src-tauri/Cargo.toml
[build-dependencies]
tauri-ts-generator = "2.1"Then drive the pipeline from your build script. A Tauri project already has a build.rs
calling tauri_build::build() — add to it, don't replace it:
// src-tauri/build.rs
use std::path::PathBuf;
use tauri_ts_generator::config::{Config, InputConfig, NamingConfig, OutputConfig};
use tauri_ts_generator::pipeline::Pipeline;
fn main() {
tauri_build::build(); // keep the existing Tauri build step
// Only regenerate when the Rust command sources change.
println!("cargo:rerun-if-changed=src");
let config = Config {
input: InputConfig {
source_dir: PathBuf::from("src"),
exclude: vec!["tests".into()],
use_cargo_expand: false,
cargo_manifest: None,
},
output: OutputConfig {
// Write into the frontend tree, outside the src-tauri crate.
types_file: PathBuf::from("../src/bindings/types.ts"),
commands_file: PathBuf::from("../src/bindings/commands.ts"),
},
naming: NamingConfig::default(),
};
// Don't fail the whole build if codegen hits a snag — surface a warning instead.
if let Err(e) = Pipeline::new(false).run(&config) {
println!("cargo:warning=tauri-ts-generator: {e}");
}
}Three things worth knowing:
- Write the output outside the
src-tauricrate (e.g.../src/bindings, in the frontend tree). If you write intosrc-tauri/srcwhile also usingrerun-if-changed=src, you get a rebuild loop: the build script writes a file undersrc, Cargo seessrcchange, and runs the build script again. The frontend dir is outside the crate, so writing there doesn't retrigger the Rust build. - Keep
use_cargo_expand = falsehere. That flag shells out tocargo expand, i.e. it runs Cargo on top of the build that's already in progress. For macro-generated commands, prefer a separate CLI run over doing it frombuild.rs. - The generator pulls in
synandclap, so it adds a bit to your build-dependency compile time. If that bothers you, stick with the CLI and apackage.jsonscript / Makefile target.
Customize the generator behavior using the TOML configuration file.
Defines where the generator looks for code.
| Key | Description | Default |
|---|---|---|
source_dir |
Root directory of your Rust source code. | "src-tauri/src" |
exclude |
List of directories or files to ignore. | ["tests", "target"] |
use_cargo_expand |
Enable to discover macro-generated types and #[tauri::command] functions (requires cargo-expand installed). |
false |
cargo_manifest |
Path to Cargo.toml for cargo-expand. Auto-detected if empty. |
None |
Defines where the generated TypeScript files are saved.
| Key | Description | Default |
|---|---|---|
types_file |
Path for generated interfaces/types. | "src/generated/types.ts" |
commands_file |
Path for generated invoke functions. | "src/generated/commands.ts" |
Customize naming conventions for generated types and functions.
| Key | Description | Default |
|---|---|---|
type_prefix |
Prefix added to all generated interface names (e.g., "I"). | "" |
type_suffix |
Suffix added to all generated interface names (e.g., "DTO"). | "" |
function_prefix |
Prefix for generated command functions. | "" |
function_suffix |
Suffix for generated command functions. | "" |
The generator maps Rust types to TypeScript as follows:
| Rust Type | TypeScript Type |
|---|---|
String, &str, char |
string |
i8...i64, u8...u64, f32, f64 |
number |
bool |
boolean |
Option<T> |
T | null (default), or optional field ?: T (with #[ts(optional)]) |
Vec<T> |
T[] |
HashMap<K, V> |
Record<K, V> (if K is string/number) |
Box<T>, Arc<T>, Rc<T>, Cow<'_, T> |
T (transparent — matches serde's serialization) |
Result<T, E> |
Promise<T> (in return types) |
() / Unit |
void |
bytes::Bytes |
number[] |
serde_json::Value |
unknown |
Common types from popular crates are mapped automatically:
- Chrono:
DateTime,NaiveDate,NaiveTime→string - Time:
OffsetDateTime,Date→string - Uuid:
Uuid→string - Url:
Url→string - Rust Decimal:
Decimal→string - Std:
Path,PathBuf,IpAddr→string;Duration→number
Rust:
#[derive(Serialize)]
pub struct User {
pub id: i32,
pub name: String,
}
#[tauri::command]
pub async fn get_user(id: i32) -> Result<User, String> { /* ... */ }TypeScript Output:
export interface User {
id: number;
name: string;
}
export async function getUser(id: number): Promise<User> {
return invoke<User>("get_user", { id });
}Field names in the generated TypeScript match exactly what serde will produce in JSON. Without any serde attributes, the Rust field name is preserved as-is (so a snake_case field stays snake_case in TypeScript). Use #[serde(rename = "...")] or #[serde(rename_all = "...")] to control the output.
Rust:
#[derive(Serialize)]
pub struct Config {
#[serde(rename = "API_KEY")]
pub api_key: String,
pub retries: i32,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct User {
pub user_id: i32,
pub first_name: String,
}TypeScript Output:
export interface Config {
API_KEY: string; // Exact rename
retries: number; // No serde attrs → name preserved as-is
}
export interface User {
userId: number; // rename_all = "camelCase"
firstName: string;
}Supports various serde representations.
Rust:
#[derive(Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Status {
Active,
Inactive,
}TypeScript Output:
export type Status = "ACTIVE" | "INACTIVE";Supported rename_all values:
lowercase,UPPERCASEcamelCase,PascalCasesnake_case,SCREAMING_SNAKE_CASEkebab-case,SCREAMING-KEBAB-CASE
Use rename_all on commands to control argument keys in the invoke payload.
Rust:
#[tauri::command(rename_all = "snake_case")]
pub fn update_user(user_id: i32, new_email: String) { /* ... */ }TypeScript Output:
export async function updateUser(userId: number, newEmail: string): Promise<void> {
// Arguments are mapped to snake_case in the payload
return invoke<void>("update_user", { user_id: userId, new_email: newEmail });
}By default, Option<T> maps to T | null. You can use the #[ts(optional)] attribute to map it to prop?: T instead.
Note: You must add
#[derive(tauri_ts_generator::TS)]to enable the#[ts(...)]attribute on your structs.
Rust:
use tauri_ts_generator::TS;
#[derive(Serialize, TS)]
pub struct Config {
pub name: Option<String>,
#[ts(optional)]
pub volume: Option<f32>,
}TypeScript Output:
export interface Config {
name: string | null; // Default behavior
volume?: number; // With #[ts(optional)]
}Fields with #[serde(skip)] are excluded from the TypeScript output. Note that skip_serializing and skip_deserializing are not excluded, as they only affect one direction of serialization.
Rust:
#[derive(Serialize)]
pub struct User {
pub id: i32,
pub name: String,
#[serde(skip)]
pub internal_cache: Vec<u8>, // Excluded from TypeScript
#[serde(skip_serializing)]
pub password_hash: String, // Kept in TypeScript (needed for input)
}TypeScript Output:
export interface User {
id: number;
name: string;
passwordHash: string; // skip_serializing fields are kept
// internal_cache is excluded due to #[serde(skip)]
}Use #[serde(flatten)] to embed one struct's fields into another. The generator produces TypeScript intersection types.
Rust:
#[derive(Serialize)]
pub struct Address {
pub city: String,
pub country: String,
}
#[derive(Serialize)]
pub struct User {
pub name: String,
#[serde(flatten)]
pub address: Address,
}TypeScript Output:
export interface Address {
city: string;
country: string;
}
export type User = {
name: string;
} & Address;This works correctly for both command arguments (input) and return types (output).
tauri-ts-generator <COMMAND> [OPTIONS]
Commands:
generate Generate TypeScript bindings
init Create a default configuration file
help Print help information
Options:
-v, --verbose Enable verbose logging (useful for debugging scanning/parsing)
-c, --config Path to config file (default: tauri-codegen.toml)MIT