Skip to content

Repository files navigation

tauri-ts-generator

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.

Features

  • Automated Scanning: Recursively scans your src-tauri directory 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 on Option fields to generate prop?: T instead of T | null.
    • Provides #[derive(tauri_ts_generator::TS)] to register the ts attribute 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, and rust_decimal::Decimal.
  • Async Handling: Correctly generates Promise<T> for async commands.
  • Tauri Integration:
    • Automatically imports invoke from @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 as Channel<T> in the generated signature (with the Channel import added when needed), plus an exported <Command><Arg>ChannelType alias for each channel payload in the types file (since v2.1.0).
  • Macro Support: Optional integration with cargo-expand resolves both types and #[tauri::command] functions generated by macros. A macro_rules! that stamps out N typed commands (or a proc-macro like progenitor) is invisible to a raw-source parser; with use_cargo_expand = true those commands appear in the generated TypeScript automatically (since v2.0.3).
  • Conflict Resolution: Detects and handles naming conflicts or ambiguous imports.

Installation

cargo install tauri-ts-generator

Or run directly from source:

cargo run --release -- generate

Quick Start

  1. Initialize Configuration (Run in your Tauri project root):

    tauri-ts-generator init

    This creates a tauri-codegen.toml file.

  2. Generate Bindings:

    tauri-ts-generator generate

Build script (build.rs) integration

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-tauri crate (e.g. ../src/bindings, in the frontend tree). If you write into src-tauri/src while also using rerun-if-changed=src, you get a rebuild loop: the build script writes a file under src, Cargo sees src change, 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 = false here. That flag shells out to cargo 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 from build.rs.
  • The generator pulls in syn and clap, so it adds a bit to your build-dependency compile time. If that bothers you, stick with the CLI and a package.json script / Makefile target.

Configuration (tauri-codegen.toml)

Customize the generator behavior using the TOML configuration file.

[input] Section

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

[output] Section

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"

[naming] Section

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. ""

Type Mappings

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

Supported External Types

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

Examples

1. Basic Command & Struct

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 });
}

2. Field Naming (Matches Serde JSON)

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;
}

3. Enums

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, UPPERCASE
  • camelCase, PascalCase
  • snake_case, SCREAMING_SNAKE_CASE
  • kebab-case, SCREAMING-KEBAB-CASE

4. Command Arguments Rename

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 });
}

5. Option with Undefined

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)]
}

6. Skipping Fields

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)]
}

7. Serde Flatten (Intersection Types)

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).

CLI Reference

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)

License

MIT

About

No description, website, or topics provided.

Resources

Stars

7 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages