WARNING: These docs are a work in progress! Some sections are incomplete or incorrect.

v2

Installation

[!WARNING] Tauri Specta v2 is still in beta, until Tauri v2 lands as stable.

However, it's safe to use as long as you lock your versions.

cargo add tauri@=2.0.0-beta.22
cargo add specta@=2.0.0-rc.12
cargo add tauri-specta@=2.0.0-rc.11 --features javascript,typescript

Adding Specta to custom types

use specta::Type;
use serde::{Deserialize, Serialize};
 
// The `specta::Type` macro allows us to understand your types
// We implement `specta::Type` on primitive types for you.
// If you want to use a type from an external crate you may need to enable the feature on Specta.
#[derive(Serialize, Type)]
pub struct MyCustomReturnType {
    pub some_field: String,
}
 
#[derive(Deserialize, Type)]
pub struct MyCustomArgumentType {
    pub foo: String,
    pub bar: i32,
}

Annotate your Tauri commands with Specta

#[tauri::command]
#[specta::specta] // <-- This bit here
fn greet3() -> MyCustomReturnType {
    MyCustomReturnType {
        some_field: "Hello World".into(),
    }
}
 
#[tauri::command]
#[specta::specta] // <-- This bit here
fn greet(name: String) -> String {
  format!("Hello {name}!")
}

Export your bindings

use specta::collect_types;
use tauri_specta::{ts, js};
 
// this example exports your types on startup when in debug mode. You can do whatever.
 
fn main() {
    let invoke_handler = {
        // You can use `tauri_specta::js::builder` for exporting JS Doc instead of Typescript!`
        let builder = tauri_specta::ts::builder()
            .commands(tauri_specta::collect_commands![greet, greet2, greet3 ]); // <- Each of your commands
 
 
        #[cfg(debug_assertions)] // <- Only export on non-release builds
        let builder = builder.path("../src/bindings.ts");
 
        builder.build().unwrap()
    };
 
    tauri::Builder::default()
        .invoke_handler(invoke_handler)
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Usage on frontend

import * as commands from "./bindings"; // This should point to the file we export from Rust
 
await commands.greet("Brendan");

Events

You must reconfigure your builder to support events:

fn main() {
-    let invoke_handler = {
+    let (invoke_handler, register_events) = {
        // You can use `tauri_specta::js::builder` for exporting JS Doc instead of Typescript!`
        let builder = tauri_specta::ts::builder()
            .commands(tauri_specta::collect_commands![greet, greet2, greet3 ]) // <- Each of your commands
+           .events(tauri_specta::collect_events![]); // This should contain all your events.
 
        #[cfg(debug_assertions)] // <- Only export on non-release builds
        let builder = builder.path("../src/bindings.ts");
 
        builder.build().unwrap()
    };
 
    tauri::Builder::default()
        .invoke_handler(invoke_handler)
+       .setup(|app| {
+           register_events(app);
+           Ok(())
+       })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Now you can add your first event. You can add as many of these as your want.

#[derive(Debug, Clone, Serialize, Deserialize, specta::Type, tauri_specta::Event)]
pub struct DemoEvent(String);

and make sure you register the event with Tauri Specta by adding it to the collect_events macro like the following:

.events(tauri_specta::collect_events![DemoEvent]); // This should contain all your events, comma separated.

Finally, you can setup a listener or emit a message from Rust like the following:

tauri::Builder::default()
    .invoke_handler(invoke_handler)
    .setup(|app| {
        let handle = app.handle();
 
        DemoEvent::listen_global(&handle, |event| {
            dbg!(event.payload);
        });
 
        DemoEvent("Test".to_string()).emit_all(&handle).unwrap();
    })
    ...

Most methods take a handle which can be any of the following types:

The Event trait defines all methods that can be used to emit or listen so refer to it.

and it can be used in TS like the following:

import { commands, events } from "./bindings";
import { appWindow } from "@tauri-apps/api/window";
 
// For all windows
events.demoEvent.listen((e) => console.log(e));
 
// For a single window
events.demoEvent(appWindow).listen((e) => console.log(e));
 
// Emit to the backend and all windows
await events.demoEvent.emit("Test");
 
// Emit to a window
await events.demoEvent(appWindow).emit("Test");

Extra Types

Sometimes you might want to export a type to Typescript but it doesn't actually show up in any of our commands.

You can do that like the following:

let builder = ts::builder()
    // < your commands and events are probaly here
    .types(TypeCollection::default().register::<Custom>()); // < call `register` as much as you want.

register only supports named types as otherwise you would run into the following problem:

//     vvv      - What would this be without a name?
export ... = {};

Any type implemented using the Type derive macro will meet this requirement.

Constants

It may be useful to export a constant from your Rust into your Typescript. You can do this like the following:

let builder = ts::builder()
    // < your commands and events are probaly here
    .types(StaticCollection::default().register("myConstant", 42)); // < call `register` as much as you want.

This value must implement serde::Serialize.

File header

It's very common that your are using a linting or formatting tool on your codebase and it's likely that the output of Tauri Specta will not match your style. You can configure the header of the file like the following to solve this:

let builder = ts::builder()
    // < your commands and events are probally here
    .header("// @ts-nocheck");
Edit on GitHub

Last updated on

On this page