Rust コマンドが返した値は JSON にシリアライズされ、invoke の Promise の解決値としてフロントエンドに届きます。文字列や数値ならそのまま、構造体は serde::Serialize を derive するだけで JS のオブジェクトになります。このレシピでは「Rust の型が JS でどんな値になるか」をフロントエンド視点で整理し、Rust 側の書き方は 2 章にまとめます。Err を返したときの受け方は Rust 側で起きたエラーを JS でキャッチする を参照してください。
前提条件
追加プラグインや権限設定は不要です。構造体を返すには serde の derive 機能が必要ですが、テンプレートの src-tauri/Cargo.toml には最初から入っています。
1. フロントエンドから実装する (TypeScript)
invoke<T> の T は「返ってくる JSON をこう解釈する」という 宣言 で、実行時の検証はありません。Rust の構造体と同じ形の interface を用意し、フィールド名の変換ルールを揃えるのがポイントです。
// src/main.ts
import { invoke } from '@tauri-apps/api/core';
// Rust の struct User に対応する型(Rust 側は rename_all = "camelCase" 前提)
interface User {
id: number;
username: string;
isActive: boolean;
email: string | null; // Option<String> は null になる
}
async function showUser() {
const user = await invoke<User>('get_current_user'); // 構造体 -> オブジェクト
const users = await invoke<User[]>('list_users'); // Vec -> 配列
const found = await invoke<User | null>('find_user', { id: 42 }); // Option
console.log(user.username, users.length);
if (found === null) console.log('該当ユーザーなし');
}
型対応の早見表
| Rust の型 | JS で受け取る値 |
|---|---|
String, &str | string |
i32, u32, f64 など | number |
bool | boolean |
() | undefined |
Option<T> | T または null |
Vec<T> | T[](Vec<u8> も 数値の配列) |
#[derive(Serialize)] struct | オブジェクト |
tauri::ipc::Response | ArrayBuffer |
大きなバイナリは Response で受け取る
Vec<u8> をそのまま返すと [137, 80, 78, ...] のような JSON 配列(number[])になり、数 MB のファイルでは文字列化とパースのコストが目立ちます。Rust 側で tauri::ipc::Response に包むと JSON を経由せずに ArrayBuffer で届きます。
import { invoke } from '@tauri-apps/api/core';
const buf = await invoke<ArrayBuffer>('read_image');
const blob = new Blob([new Uint8Array(buf)], { type: 'image/png' });
document.querySelector<HTMLImageElement>('#preview')!.src = URL.createObjectURL(blob);
2. バックエンドから実装する (Rust)
フィールド名を JS の慣習に合わせるため、構造体に #[serde(rename_all = "camelCase")] を付けています。
// src-tauri/src/lib.rs
use serde::Serialize;
use tauri::ipc::Response;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct User {
id: u32,
username: String,
is_active: bool, // JS では isActive
email: Option<String>, // None は null
}
#[tauri::command]
fn get_current_user() -> User {
User { id: 1, username: "admin".into(), is_active: true, email: None }
}
#[tauri::command]
fn list_users() -> Vec<User> { vec![get_current_user()] }
#[tauri::command]
fn find_user(id: u32) -> Option<User> {
if id == 1 { Some(get_current_user()) } else { None }
}
#[tauri::command]
fn read_image() -> Result<Response, String> {
let data = std::fs::read("assets/sample.png").map_err(|e| e.to_string())?;
Ok(Response::new(data))
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
get_current_user, list_users, find_user, read_image
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
serde の属性で JS に届く形を変える
Option<T> の None は既定では null で届きますが、#[serde(skip_serializing_if = "Option::is_none")] を付けるとキーごと省かれ、JS では undefined になります。TypeScript の型も string | null ではなく省略可能なプロパティになるので、アプリの中でどちらかに揃えます。パスワードのハッシュのように JS へ渡したくないフィールドは #[serde(skip)] で外せます。画面用に別の構造体を作らずに済みます。
use serde::Serialize;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Profile {
display_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
avatar_url: Option<String>, // None ならキーごと省く(JS では undefined)
#[serde(skip)]
password_hash: String, // JS には送らない
}
#[tauri::command]
fn get_profile() -> Profile {
Profile {
display_name: "admin".into(),
avatar_url: None,
password_hash: String::from("dummy"),
}
}
get_profile もほかのコマンドと同じく generate_handler! に登録します。受け取る側では、省かれうるフィールドを ? 付きのプロパティにします。
import { invoke } from '@tauri-apps/api/core';
interface Profile {
displayName: string;
avatarUrl?: string; // キーが無いことがある(null ではない)
}
const profile = await invoke<Profile>('get_profile');
console.log(profile.avatarUrl ?? '(未設定)'); // ?? なら null と undefined の両方に効く
動作確認
npm run tauri dev で起動し、showUser() を実行して user を console.log すると次のように表示されます。
{ id: 1, username: "admin", isActive: true, email: null }
rename_all を外すと is_active のままになり、user.isActive は undefined になります(型エラーにはなりません)。read_image の戻り値は ArrayBuffer(12345) のように表示されます。
よくあるエラーと対処法
the trait bound MyStruct: serde::Serialize is not satisfied という趣旨のコンパイルエラー
戻り値の型(またはそのフィールドの型)が Serialize を実装していません。自作の構造体には #[derive(Serialize)] を付けます。std::fs::File や Mutex のようなリソース型は返せないので、必要な情報だけを取り出した構造体に詰め替えます。
値は届いているのに undefined になるフィールドがある
Rust の snake_case と TypeScript の camelCase の食い違いです。構造体に #[serde(rename_all = "camelCase")] を付けるか、interface 側を is_active に合わせます。どちらかに統一しないと、Rust に送り返すときにも同じ問題が起きます。
OS ごとの違いと注意点
シリアライズの挙動は OS に依存しません。注意点はデータの中身と量です。
- パスを返すとき: Windows では
C:\\Users\\...のようにバックスラッシュ区切りの文字列になります。表示用に/へ置換するなど、フロント側で扱いを決めておきます。 u64/i64の大きな値: JS のnumberは 2^53 までしか正確に表せません。64 bit の ID などは Rust 側でStringにして返します。- 大きなデータの往復: 数十 MB の JSON を 1 回で返すとメモリと時間を大きく消費します。一覧はページングし、バイナリは
Response、連続データはイベントかChannelに切り替えます。 enumの形式: データ付きバリアントは既定で{ "Variant": {...} }の外部タグ形式になります。JS で扱いやすくするには#[serde(tag = "type")]を検討してください(Serde で JSON のシリアライズを行う)。
