tauri-plugin-typegen
v0.1.5
Published
A command-line tool that automatically generates TypeScript models and bindings from your Tauri commands, eliminating the manual process of creating frontend types and validation.
Readme
Tauri TypeGen
A command-line tool that automatically generates TypeScript models and bindings from your Tauri commands, eliminating the manual process of creating frontend types and validation.
Features
- 🔍 Automatic Discovery: Scans your Rust source files to find all
#[tauri::command]functions - 📝 TypeScript Generation: Creates TypeScript interfaces for all command parameters and return types
- ✅ Validation Support: Generates validation schemas using Zod or plain TypeScript types
- 🚀 Command Bindings: Creates strongly-typed frontend functions that call your Tauri commands
- 🎯 Type Safety: Ensures frontend and backend types stay in sync
- 🛠️ CLI Integration: Generate types with a simple command:
cargo tauri-typegen generate - 📊 Dependency Visualization: Optional dependency graph generation for complex projects
- ⚙️ Configuration Support: Supports both standalone config files and Tauri project integration
Table of Contents
- Quick Start
- Installation
- Usage Examples
- TypeScript Compatibility
- API Reference
- Configuration Options
- Development
Quick Start
Install the CLI tool:
cargo install tauri-plugin-typegenGenerate TypeScript bindings from your Tauri project:
# In your Tauri project root cargo tauri-typegen generateUse the generated bindings in your frontend:
import { createUser, getUsers } from './src/generated'; const user = await createUser({ request: { name: "John", email: "[email protected]" } }); const users = await getUsers({ filter: null });
CLI Commands
cargo tauri-typegen generate [OPTIONS]
Options:
-p, --project-path <PATH> Path to Tauri source directory [default: ./src-tauri]
-o, --output-path <PATH> Output path for TypeScript files [default: ./src/generated]
-v, --validation <LIBRARY> Validation library: zod or none [default: zod]
--verbose Verbose output
--visualize-deps Generate dependency graph visualization
-c, --config <CONFIG_FILE> Configuration file pathcargo tauri-typegen init [OPTIONS]
Options:
-p, --project-path <PATH> Path to Tauri source directory [default: ./src-tauri]
-g, --generated-path <PATH> Output path for generated files [default: ./src/generated]
-o, --output <PATH> Output path for config file [default: tauri.conf.json]
-v, --validation <LIBRARY> Validation library: zod or none [default: zod]
--verbose Verbose output
--visualize-deps Generate dependency graph visualization
--force Force overwrite existing configurationInstallation
CLI Tool Installation
Install the CLI tool globally:
cargo install tauri-plugin-typegenTypeScript Bindings (Optional)
If you need TypeScript bindings for frontend integration:
npm install @thwbh/tauri-plugin-typegenNote: This plugin requires manual installation. The cargo tauri add command only works with official Tauri plugins.
Configuration Setup
Initialize Configuration
Add typegen configuration to your existing Tauri project:
# Add configuration to your existing tauri.conf.json (default)
cargo tauri-typegen init
# Specify custom validation library
cargo tauri-typegen init --validation none
# Create a standalone config file
cargo tauri-typegen init --output my-config.json --validation zodNote: The init command requires an existing tauri.conf.json file in your Tauri project. It will update the file to add the plugins.typegen configuration section.
Configuration File
Configuration can be stored in a standalone JSON file or within your tauri.conf.json:
{
"project_path": "./src-tauri",
"output_path": "./src/generated",
"validation_library": "zod",
"verbose": true,
"visualize_deps": false
}Package.json Integration
Add generation to your build scripts:
{
"scripts": {
"generate-types": "cargo tauri-typegen generate",
"tauri:dev": "npm run generate-types && tauri dev",
"tauri:build": "npm run generate-types && tauri build"
}
}Usage Examples
Example: E-commerce App
Let's say you have these Tauri commands in your Rust backend:
src-tauri/src/commands/products.rs:
use serde::{Deserialize, Serialize};
use tauri::command;
use validator::Validate;
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Product {
pub id: i32,
pub name: String,
pub description: String,
pub price: f64,
pub in_stock: bool,
pub category_id: i32,
}
#[derive(Debug, Deserialize, Validate)]
#[serde(rename_all = "camelCase")]
pub struct CreateProductRequest {
#[validate(length(min = 1, max = 100))]
pub name: String,
#[validate(length(max = 500))]
pub description: String,
#[validate(range(min = 0.01, max = 10000.0))]
pub price: f64,
pub category_id: i32,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProductFilter {
pub search: Option<String>,
pub category_id: Option<i32>,
pub min_price: Option<f64>,
pub max_price: Option<f64>,
pub in_stock_only: Option<bool>,
}
#[command]
pub async fn create_product(request: CreateProductRequest) -> Result<Product, String> {
// Implementation here
Ok(Product {
id: 1,
name: request.name,
description: request.description,
price: request.price,
in_stock: true,
category_id: request.category_id,
})
}
#[command]
pub async fn get_products(filter: Option<ProductFilter>) -> Result<Vec<Product>, String> {
// Implementation here
Ok(vec![])
}
#[command]
pub async fn delete_product(id: i32) -> Result<(), String> {
// Implementation here
Ok(())
}Generate TypeScript Bindings
Command Line Generation
Generate bindings with the CLI tool:
# Basic generation with defaults
cargo tauri-typegen generate
# Custom paths and validation
cargo tauri-typegen generate \
--project-path ./src-tauri \
--output-path ./src/lib/generated \
--validation zod \
--verbose
# Generate with dependency visualization
cargo tauri-typegen generate --visualize-deps
# Use configuration file
cargo tauri-typegen generate --config my-config.json
# Quick examples for different setups
cargo tauri-typegen generate -p ./backend -o ./frontend/types -v zod
cargo tauri-typegen generate --validation none # No validation schemasBuild Integration
The recommended approach is to use Tauri's built-in build hooks to ensure types are generated before the frontend build starts. This solves the chicken-and-egg problem where the frontend needs the generated types but builds before the Rust backend.
Method 1: Tauri Build Hooks (Recommended)
First, add configuration to your tauri.conf.json:
{
"build": {
"beforeDevCommand": "cargo tauri-typegen generate && npm run dev",
"beforeBuildCommand": "cargo tauri-typegen generate && npm run build",
"devUrl": "http://localhost:1420",
"frontendDist": "../dist"
},
"plugins": {
"tauri-typegen": {
"project_path": "./src-tauri",
"output_path": "./src/generated",
"validation_library": "zod",
"verbose": false,
"visualize_deps": false
}
}
}Then use standard Tauri commands:
# Development - types generated automatically before frontend starts
npm run tauri dev
# Production build - types generated before frontend build
npm run tauri buildMethod 2: Package.json Scripts (Alternative)
If you prefer explicit control in package.json:
{
"scripts": {
"generate-types": "cargo tauri-typegen generate",
"dev": "npm run generate-types && npm run tauri dev",
"build": "npm run generate-types && npm run tauri build",
"tauri": "tauri"
}
}Method 3: Cargo Build Scripts (Advanced)
For tighter integration, add type generation to your Rust build process.
Add tauri-typegen as build dependency to your project.
cargo add --build tauri-plugin-typegenIn src-tauri/build.rs:
fn main() {
// Generate TypeScript bindings before build
tauri_plugin_typegen::BuildSystem::generate_at_build_time()
.expect("Failed to generate TypeScript bindings");
tauri_build::build()
}Generated Files Structure
After running the generator:
src/generated/
├── types.ts # TypeScript interfaces
├── schemas.ts # Zod validation schemas (if using zod)
├── commands.ts # Typed command functions
├── index.ts # Barrel exports
├── dependency-graph.txt # Text dependency visualization (if --visualize-deps)
└── dependency-graph.dot # DOT format graph (if --visualize-deps)Generated types.ts:
export interface Product {
id: number;
name: string;
description: string;
price: number;
inStock: boolean;
categoryId: number;
}
export interface CreateProductRequest {
name: string;
description: string;
price: number;
categoryId: number;
}
export interface CreateProductParams {
request: CreateProductRequest;
}
export interface GetProductsParams {
filter?: ProductFilter | null;
}
export interface DeleteProductParams {
id: number;
}Generated commands.ts:
import { invoke } from '@tauri-apps/api/core';
import * as schemas from './schemas';
import type * as types from './types';
export async function createProduct(params: types.CreateProductParams): Promise<types.Product> {
const validatedParams = schemas.CreateProductParamsSchema.parse(params);
return invoke('create_product', validatedParams);
}
export async function getProducts(params: types.GetProductsParams): Promise<types.Product[]> {
const validatedParams = schemas.GetProductsParamsSchema.parse(params);
return invoke('get_products', validatedParams);
}
export async function deleteProduct(params: types.DeleteProductParams): Promise<void> {
const validatedParams = schemas.DeleteProductParamsSchema.parse(params);
return invoke('delete_product', validatedParams);
}Using Generated Bindings in Frontend
React Example
import React, { useEffect, useState } from 'react';
import { getProducts, createProduct, deleteProduct } from '../lib/generated';
import type { Product, ProductFilter } from '../lib/generated';
export function ProductList() {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadProducts();
}, []);
const loadProducts = async () => {
try {
setLoading(true);
const result = await getProducts({ filter: null });
setProducts(result);
} catch (error) {
console.error('Failed to load products:', error);
} finally {
setLoading(false);
}
};
const handleCreateProduct = async () => {
try {
const newProduct = await createProduct({
request: {
name: 'New Product',
description: 'A new product',
price: 19.99,
categoryId: 1,
}
});
setProducts([...products, newProduct]);
} catch (error) {
console.error('Failed to create product:', error);
}
};
const handleDeleteProduct = async (productId: number) => {
try {
await deleteProduct({ id: productId });
setProducts(products.filter(p => p.id !== productId));
} catch (error) {
console.error('Failed to delete product:', error);
}
};
if (loading) return <div>Loading...</div>;
return (
<div>
<h2>Products</h2>
<button onClick={handleCreateProduct}>Create Product</button>
<div className="products">
{products.map((product) => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>{product.description}</p>
<p>${product.price}</p>
<p>Stock: {product.inStock ? '✅' : '❌'}</p>
<button onClick={() => handleDeleteProduct(product.id)}>
Delete
</button>
</div>
))}
</div>
</div>
);
}Vue Example
<template>
<div class="product-manager">
<h2>Product Manager</h2>
<form @submit.prevent="createProduct" class="create-form">
<input v-model="newProduct.name" placeholder="Product name" required />
<textarea v-model="newProduct.description" placeholder="Description"></textarea>
<input v-model.number="newProduct.price" type="number" step="0.01" placeholder="Price" required />
<button type="submit">Create Product</button>
</form>
<div class="products-list">
<div v-for="product in products" :key="product.id" class="product-item">
<h4>{{ product.name }}</h4>
<p>{{ product.description }}</p>
<p>${{ product.price }}</p>
<button @click="deleteProduct(product.id)">Delete</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { getProducts, createProduct as createProductCmd, deleteProduct as deleteProductCmd } from '../lib/generated';
import type { Product, CreateProductRequest } from '../lib/generated';
const products = ref<Product[]>([]);
const newProduct = ref<CreateProductRequest>({
name: '',
description: '',
price: 0,
categoryId: 1,
});
onMounted(async () => {
await loadProducts();
});
const loadProducts = async () => {
try {
const result = await getProducts({ filter: null });
products.value = result;
} catch (error) {
console.error('Failed to load products:', error);
}
};
const createProduct = async () => {
try {
const product = await createProductCmd({ request: { ...newProduct.value } });
products.value.push(product);
newProduct.value = { name: '', description: '', price: 0, categoryId: 1 };
} catch (error) {
console.error('Failed to create product:', error);
}
};
const deleteProduct = async (id: number) => {
try {
await deleteProductCmd({ id });
products.value = products.value.filter(p => p.id !== id);
} catch (error) {
console.error('Failed to delete product:', error);
}
};
</script>Svelte Example
src/lib/ProductStore.ts:
import { writable } from 'svelte/store';
import { getProducts, createProduct, deleteProduct } from './generated';
import type { Product, ProductFilter } from './generated';
export const products = writable<Product[]>([]);
export const loading = writable(false);
export const productStore = {
async loadProducts(filter: ProductFilter = {}) {
loading.set(true);
try {
const result = await getProducts({ filter });
products.set(result);
} catch (error) {
console.error('Failed to load products:', error);
} finally {
loading.set(false);
}
},
async createProduct(request: CreateProductRequest) {
try {
const newProduct = await createProduct({ request });
products.update(items => [...items, newProduct]);
return newProduct;
} catch (error) {
console.error('Failed to create product:', error);
throw error;
}
},
async deleteProduct(id: number) {
try {
await deleteProduct({ id });
products.update(items => items.filter(p => p.id !== id));
} catch (error) {
console.error('Failed to delete product:', error);
throw error;
}
}
};Benefits of Using Generated Bindings
✅ Type Safety
// ❌ Before: Manual typing, prone to errors
const result = await invoke('create_product', {
name: 'Product',
price: '19.99', // Oops! Should be number
category_id: 1 // Oops! Should be camelCase
});
// ✅ After: Generated bindings with validation
const result = await createProduct({
request: {
name: 'Product',
price: 19.99, // Correct type
categoryId: 1 // Correct naming
}
});✅ Runtime Validation
// Automatically validates input at runtime
try {
await createProduct({
request: {
name: '', // Will throw validation error
price: -5 // Will throw validation error
}
});
} catch (error) {
console.error('Validation failed:', error);
}✅ IntelliSense & Autocomplete
Your IDE will provide full autocomplete and type hints for all generated functions and types.
✅ Automatic Updates
When you change your Rust commands, just re-run the generator to get updated TypeScript bindings.
TypeScript Compatibility
The generated TypeScript code is compatible with modern TypeScript environments and follows current best practices.
Version Requirements
- TypeScript 3.7+ (for optional chaining support)
- ES2018+ compilation target
- Zod 3.x (when using Zod validation)
Generated Code Features
The generated TypeScript code uses modern language features:
- ES Modules:
import/exportstatements - Async/Await: All command functions are async
- Union Types:
string | null, optional properties - Generic Types:
Array<T>,Promise<T>,Record<K, V> - Tuple Types:
[string, number]for Rust tuples - Template Literal Types: Advanced string manipulation (when needed)
TypeScript Configuration
Ensure your tsconfig.json is compatible with the generated code:
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
}
}Generated Type Mappings
| Rust Type | Generated TypeScript | Notes |
|-----------|---------------------|-------|
| String, &str | string | Basic string types |
| i32, f64, etc. | number | All numeric types → number |
| bool | boolean | Boolean type |
| () | void | Unit type |
| Option<T> | T \| null | Nullable types |
| Vec<T> | T[] | Arrays |
| HashMap<K, V> | Map<K, V> | Map type |
| BTreeMap<K, V> | Map<K, V> | Consistent with HashMap |
| HashSet<T> | T[] | Arrays for JSON compatibility |
| (T, U) | [T, U] | Tuple types |
| Result<T, E> | T | Errors handled by Tauri runtime |
API Reference
CLI Commands
cargo tauri-typegen generate [OPTIONS]
OPTIONS:
-p, --project-path <PATH> Path to the Tauri project source directory
[default: ./src-tauri]
-o, --output-path <PATH> Output path for generated TypeScript files
[default: ./src/generated]
-v, --validation <LIBRARY> Validation library to use
[default: zod] [possible values: zod, none]
--verbose Enable verbose output
--visualize-deps Generate dependency graph visualization
-c, --config <CONFIG_FILE> Configuration file path
-h, --help Print help informationLibrary Usage (Advanced)
For programmatic usage in build scripts:
use tauri_plugin_typegen::interface::{GenerateConfig, generate_from_config};
let config = GenerateConfig {
project_path: "./src-tauri".to_string(),
output_path: "./src/generated".to_string(),
validation_library: "zod".to_string(),
verbose: Some(true),
visualize_deps: Some(false),
..Default::default()
};
let files = generate_from_config(&config)?;Configuration Options
Validation Libraries
zod- Generates Zod schemas with validationnone- No validation schemas generated, only TypeScript types
Example Project Structure
my-tauri-app/
├── src-tauri/
│ ├── src/
│ │ ├── commands/
│ │ │ ├── user.rs # Contains #[command] functions
│ │ │ └── mod.rs
│ │ └── lib.rs
│ └── Cargo.toml
├── src/
│ ├── generated/ # Generated by this plugin
│ │ ├── types.ts
│ │ ├── schemas.ts
│ │ ├── commands.ts
│ │ └── index.ts
│ └── App.tsx
└── package.jsonDevelopment
Building the Plugin
cargo buildRunning Tests
cargo testContributing
- 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.
