thwbh/tauri-typegen(新しいタブで開きます)
主要言語: Rust
Effortless typesafety and validation for your tauri commands, powered by Zod.
- star 数
- 40
- watcher 数
- 1
- fork 数
- 14
- issue 数
- 7
- 最終更新
- 2026/10/30
主要言語: Rust
Effortless typesafety and validation for your tauri commands, powered by Zod.
A command-line tool that automatically generates TypeScript bindings from your Tauri commands, eliminating manual frontend type creation.
#[tauri::command] functionsapp.emit() eventsChannel<T> parameters#[serde(rename)] and #[serde(rename_all)] attributesInstall globally as a CLI tool:
cargo install tauri-typegen
Or add as a build dependency to your Tauri project:
cargo add --build tauri-typegen
For trying it out or one-time generation:
# Install CLI cargo install tauri-typegen # Generate types once cargo tauri-typegen generate # Use generated bindings
This 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.json
This creates a configuration block in your tauri.conf.json:
{
"plugins": {
"typegen": {
"projectPath": ".",
"outputPath": "../src/generated",
"validationLibrary": "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 automatically
use 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}!`), } );
{
"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-CASEField-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 -f, --force Force regeneration, ignoring cache
# 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
Add 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": {
"typegen": {
"projectPath": ".",
"outputPath": "../src/generated",
"validationLibrary": "zod",
"verbose": true,
"force": false
}
}
}none (default): TypeScript types only, no runtime validationzod: Generate Zod schemas with runtime validation and hooksMap external Rust types to TypeScript types for libraries like chrono, uuid, or custom types:
{
"plugins": {
"typegen": {
"projectPath": ".",
"outputPath": "../src/generated",
"validationLibrary": "zod",
"typeMappings": {
"DateTime<Utc>": "string",
"PathBuf": "string",
"Uuid": "string"
}
}
}
}Use cases:
chrono::DateTime<Utc> → stringstd::path::PathBuf → stringuuid::Uuid → stringUserId → numberExample:
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'); }
Tauri-typegen uses smart caching to skip regeneration when nothing has changed, improving build times.
A .typecache file is created in your output directory containing hashes of:
On subsequent runs, these hashes are compared. If nothing changed, generation is skipped.
To bypass the cache and force regeneration:
CLI flag (highest priority):
cargo tauri-typegen generate --force
# or
cargo tauri-typegen generate -fConfig file (tauri.conf.json):
{
"plugins": {
"typegen": {
"force": true
}
}
}Programmatic:
let mut config = GenerateConfig::default(); config.force = Some(true);
The CLI --force flag always overrides the config file value.
The cache file .typecache is stored in your output directory (e.g., ./src/generated/.typecache). Add it to .gitignore:
# Tauri-typegen cache .typecache
Or if your entire output directory is gitignored, the cache file is already excluded.
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 build
To 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 build
See the examples repository: https://github.com/thwbh/tauri-typegen-examples
This project is licensed under the MIT license.