Tauri TypeGen
A command-line tool that automatically generates TypeScript bindings from your Tauri commands, eliminating manual frontend type creation.
Features
- 🔍 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
Table of Contents
- Installation
- Quick Setup
- Recommended Setup
- Generated Code
- Using Generated Bindings
- TypeScript Compatibility
- API Reference
- Configuration
- Examples
- Contributing
Installation
Install globally as a CLI tool:
Or add as a build dependency to your Tauri project:
Quick Setup
For trying it out or one-time generation:
# Install CLI
# Generate types once
# Use generated bindings
This generates TypeScript files in ./src/generated/ from your ./src-tauri/ code.
Recommended Setup
For integrated development workflow:
1. Install and Initialize
# Install CLI
# Initialize configuration (adds to tauri.conf.json)
# Or with custom settings
This creates a configuration block in your tauri.conf.json:
2. Add Build Hook
Add tauri-typegen as a build dependency from within your Tauri project (in the src-tauri directory):
Then add to src-tauri/build.rs:
Now types auto-generate on every Rust build:
Generated Code
Example Rust Code
use ;
use Channel;
// Simple command
pub async
// Command with custom types
pub async
// Command with Channel for progress streaming
pub async
// Event emission
Generated Files
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));
}
With Zod Validation
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?.();
}
}
Using Generated Bindings
Basic Usage
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
});
With Event Listeners
import { onUserNotification } from './generated';
// Listen for events
const unlisten = await onUserNotification((message) => {
console.log('Notification:', message);
});
// Stop listening
unlisten();
React Example
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>
);
}
With Zod Validation Hooks
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 Compatibility
Requirements
- TypeScript 5.0+
- Zod 4.x (when using Zod validation)
- ES2018+ target
tsconfig.json
Type Mappings
| 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) |
Serde Attribute Support
Tauri-typegen respects serde serialization attributes to ensure generated TypeScript types match your JSON API:
Field Renaming
use ;
Generates:
export interface User {
userId: number; // Field renamed as specified
name: string;
}
Struct-Level Naming Convention
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 Precedence
Field-level rename takes precedence over struct-level rename_all:
Skip Fields
Enum Support
Enums also support serde rename attributes:
Generates:
export type MyEnum = "HELLO_WORLD" | "BYE_WORLD";
// With Zod:
export const MyEnumSchema = z.enum(["HELLO_WORLD", "BYE_WORLD"]);
Variant-level rename also works:
API Reference
CLI Commands
# Generate bindings
# Initialize configuration
Build Script API
Add as a build dependency:
Then in src-tauri/build.rs:
Programmatic API
use ;
let config = GenerateConfig ;
let files = generate_from_config?;
Configuration
Standalone Config File
Tauri Config Integration
In tauri.conf.json:
Validation Options
none(default): TypeScript types only, no runtime validationzod: Generate Zod schemas with runtime validation and hooks
Custom Type Mappings
Map external Rust types to TypeScript types for libraries like chrono, uuid, or custom types:
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 ;
use PathBuf;
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');
}
Usage in CI
When running builds in CI/CD environments, you need to generate TypeScript bindings before the frontend build step.
Why CI Needs Special Setup
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.
Recommended CI Workflow
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
Alternative: Cache the CLI Installation
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
Examples
See the examples repository: https://github.com/thwbh/tauri-typegen-examples
Contributing
- 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.