Rust の構造体を JS オブジェクトに変換する (実践編)

Recipe ID: rust-015

前のレシピ (rust-014) では Serde の設定方法を解説しました。
ここでは、実際に Tauri コマンドから多様なデータを返す際の実践的なパターンを紹介します。

1. 基本的なオブジェクトを返す

Serialize を実装した構造体は、Tauri コマンドの戻り値としてそのまま指定できます。

use serde::Serialize;

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Product {
    id: i32,
    name: String,
    price: f64,
    in_stock: bool,
}

#[tauri::command]
fn get_product() -> Product {
    Product {
        id: 101,
        name: "Mechanical Keyboard".into(),
        price: 120.50,
        in_stock: true,
    }
}

JavaScript (TypeScript) 側での受け取り:

import { invoke } from '@tauri-apps/api/core';

// 型定義をしておくと開発が楽になります
interface Product {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
}

// ジェネリクスで戻り値の型を指定
const product = await invoke<Product>('get_product');
console.log(product.name); 

2. リスト(配列)を返す

Vec<T> は JavaScript の配列 Array ([]) に変換されます。

#[tauri::command]
fn get_tags() -> Vec<String> {
    vec!["rust".into(), "tauri".into(), "typescript".into()]
}

JavaScript (TypeScript) 側での受け取り:

import { invoke } from '@tauri-apps/api/core';

const tags = await invoke<string[]>('get_tags');
console.log(tags); // ['rust', 'tauri', 'typescript']

3. マップ(辞書)を返す

HashMap<K, V> は JavaScript のオブジェクト {} または Map に変換されます。
(キーが文字列の場合はオブジェクトになります)

use std::collections::HashMap;

#[tauri::command]
fn get_metrics() -> HashMap<String, i32> {
    let mut map = HashMap::new();
    map.insert("visits".into(), 1500);
    map.insert("clicks".into(), 300);
    map
}

JavaScript (TypeScript) 側での受け取り:

import { invoke } from '@tauri-apps/api/core';

// HashMap<String, i32> は Record<string, number> に相当します
const metrics = await invoke<Record<string, number>>('get_metrics');
console.log(metrics.visits); // 1500

4. Enum (列挙型) の高度な変換

Rust の強力な Enum も JSON に変換できます。
フロントエンドでの扱いやすさを考えて、tag を指定するのがおすすめです。

おすすめ: Tagged Enum

#[serde(tag = "type")] をつけると、バリアント名を指定したフィールド(ここでは type)に入れたオブジェクトになります。

use serde::Serialize;

#[derive(Serialize)]
#[serde(tag = "type", rename_all = "camelCase")] // typeフィールドで判別、フィールド名はキャメルケース
enum WindowEvent {
    Resize { width: u32, height: u32 },
    Focus,
}

#[tauri::command]
fn get_last_event() -> WindowEvent {
    WindowEvent::Resize { width: 800, height: 600 }
}

JavaScript (TypeScript) 側:

import { invoke } from '@tauri-apps/api/core';

interface WindowEvent {
  type: 'resize' | 'focus';
  width?: number;
  height?: number;
}

// { type: "resize", width: 800, height: 600 }
// または { type: "focus" }
const event = await invoke<WindowEvent>('get_last_event');

if (event.type === 'resize') {
    // width, height の存在チェックが必要な場合がありますが、
    // Rust側で Resize バリアントなら必ず含まれるため、型定義次第でそのままアクセス可能です
    console.log(`Resized to ${event.width}x${event.height}`);
}

まとめ

  • 構造体: JS オブジェクト {} になる。rename_all="camelCase" を推奨。
  • Vec: JS 配列 [] になる。
  • Option: Some(v)vNonenull になる。
  • Enum: 設定次第だが、#[serde(tag = "...")] を使うと JS で扱いやすい。