🔌

Electronアプリで型安全なIPC通信を実現する electron-trpcという選択肢

に公開

はじめに

この記事は 3-shake Advent Calendar 2025 の記事です。

Electronアプリケーションの開発において、Main ProcessとRenderer Process間の通信(IPC)を型安全に実装することは、開発体験と保守性を高める上で重要な課題です。

本記事では、electron-trpcを用いて、IPC通信の型安全性を効率的に確保する方法について解説します。

従来の課題:型定義の分散とボイラープレート

Electron標準のIPC通信(ipcMain / ipcRenderer)を使用する場合、型安全性を確保しようとすると、記述量が増大しがちです。

一般的に、以下のような構成が必要になります。

  1. 共有の型定義ファイル: リクエスト/レスポンスの型を定義
  2. Main Process: ipcMain.handle での実装
  3. Preload Script: contextBridge での公開設定
  4. Renderer Process: window.myAPI のような型定義(d.ts

従来の実装例

// types/ipc.ts
export interface DeviceInfo {
  deviceId: string;
  deviceName: string;
  platform: string;
  version: string;
}

export interface UpdateDeviceNameRequest {
  updateName: string;
}

export interface UpdateDeviceNameResponse {
  success: boolean;
  deviceInfo: DeviceInfo;
}
// main.ts (Main Process)
import { ipcMain } from 'electron';
import type { DeviceInfo, UpdateDeviceNameRequest, UpdateDeviceNameResponse } from './types/ipc';

ipcMain.handle('device:get-info', async (): Promise<DeviceInfo> => {
  return {
    deviceId: 'id-123',
    deviceName: 'My Device',
    platform: process.platform,
    version: '1.0.0',
  };
});

ipcMain.handle('device:update-name', async (event, request: UpdateDeviceNameRequest): Promise<UpdateDeviceNameResponse> => {
  // 更新処理の実装
  return {
    success: true,
    deviceInfo: { /* ... */ },
  };
});
// preload.ts
import { contextBridge, ipcRenderer } from 'electron';
import type { UpdateDeviceNameRequest } from './types/ipc';

contextBridge.exposeInMainWorld('deviceAPI', {
  getInfo: () => ipcRenderer.invoke('device:get-info'),
  updateName: (request: UpdateDeviceNameRequest) => ipcRenderer.invoke('device:update-name', request),
});

このアプローチには以下の課題があります。

  • 定義の散在: 型定義、実装、ブリッジ、クライアント側定義と、変更箇所が多岐にわたります。
  • 手動同期のリスク: チャンネル名(文字列)の管理が必要で、リネーム時の漏れや実行時エラーのリスクがあります。
  • バリデーション: 受信データの実行時バリデーションを別途実装する必要があります。

electron-trpcによる解決

electron-trpcは、TypeScript向けのRPCフレームワークであるtRPCをElectron環境で利用できるようにするライブラリです。これを利用することで、サーバー(Main)とクライアント(Renderer)で型を共有し、ボイラープレートを大幅に削減できます。

実装例

Main Process側でルーターを定義するだけで、Renderer Process側から型推論の効いた状態で呼び出しが可能になります。

1. Main Process: ルーター定義

zodなどのスキーマバリデーションライブラリと組み合わせることで、入力値の検証も定義できます。

// presentation/trpc/router.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.create();

export const deviceRouter = t.router({
  getInfo: t.procedure.query(async () => {
    return {
      deviceInfo: {
        deviceId: 'id-123',
        deviceName: 'My Device',
        platform: process.platform,
        version: '1.0.0',
      },
    };
  }),

  updateName: t.procedure
    .input(
      z.object({
        updateName: z.string().min(1, 'Device name cannot be empty'),
      })
    )
    .mutation(async ({ input }) => {
      // 更新処理
      return { success: true };
    }),
});

export const appRouter = t.router({
  device: deviceRouter,
});

export type AppRouter = typeof appRouter;

2. Main Process: ハンドラー設定

// presentation/trpc/handler.ts
import { createIPCHandler } from 'electron-trpc/main';
import { appRouter } from './router';

createIPCHandler({
  router: appRouter,
  windows: [mainWindow],
});

3. Renderer Process: 利用側

Renderer側では、AppRouterの型情報をインポートするだけで、メソッドの補完や戻り値の型推論が機能します。

// renderer/lib/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../../presentation/trpc/router';

export const trpc = createTRPCReact<AppRouter>();
// Component.tsx
function DeviceComponent() {
  // useQueryにより、データ取得、ローディング、エラー状態が管理される
  const { data, isLoading, error } = trpc.device.getInfo.useQuery();
  const updateMutation = trpc.device.updateName.useMutation();

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>{data.deviceInfo.deviceName}</h1>
      <button onClick={() => updateMutation.mutate({ updateName: 'New Name' })}>
        Update
      </button>
    </div>
  );
}

主なメリット

  • Single Source of Truth: 型定義と実装がルーター定義(router.ts)に集約されます。
  • エンドツーエンドの型安全性: Main Processの実装を変更すると、Renderer Process側で即座に型エラーとして検知できます。
  • Zodによるバリデーション: 入力値の検証ロジックを宣言的に記述でき、不正なデータが処理に入るのを防げます。

React Queryとの統合

electron-trpc@trpc/react-queryを通じてReact Query(TanStack Query)の機能を活用できます。これにより、IPC通信における以下の機能が容易に実装できます。

  • キャッシュ管理
  • ローディング/エラー状態の管理
  • ウィンドウフォーカス時の自動再取得
  • Mutation後のデータの自動無効化(Invalidation)と再取得

従来、useEffectuseStateを組み合わせて手動で実装していた非同期処理の状態管理が、宣言的なフックの呼び出しに置き換わります。

const { data, refetch } = trpc.device.getInfo.useQuery();

const mutation = trpc.device.updateName.useMutation({
  onSuccess: () => {
    // 更新成功時に最新の情報を再取得
    refetch();
  },
});

まとめ

Electronアプリにおいて、IPC通信の型安全性を維持コストを下げつつ実現するために、electron-trpcは有力な選択肢です。特にReactを使用しているプロジェクトでは、React Queryのエコシステムをそのまま享受できる点が大きな強みとなります。

開発規模が大きくなるにつれ、型定義の分散は技術的負債になりやすいため、初期段階やリファクタリング時に導入を検討してみてはいかがでしょうか。

この記事がelectronで型安全に実装したい人の参考になれば幸いです。
ありがとうございました。

参考リンク

Discussion