C.W.K.
Stream
Lesson 07 of 07 · published

Typed IPC End to End

~14 min · tauri, typescript, ipc, architecture

Level 0Web Tourist
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"There's no compiler standing on the bridge checking both sides agree. So you build that guarantee yourself — or generate it."

The Honest Gap

When you write invoke<Note>('get_note', ...), the Note type is a claim, not a checked fact. Rust and TypeScript are compiled separately; nothing verifies that your Rust command actually returns a shape matching your TS interface. Rename a Rust field and your TS keeps compiling against a lie until it breaks at runtime. Good Tauri apps close this gap deliberately.

Strategy 1: One Typed API Module

The minimum discipline: never call invoke raw in your components. Instead, write one module that wraps every command in a typed function — getNote(id: number): Promise<Note>. Now the command name and argument shape live in exactly one place. When a command changes, you fix one file, and TypeScript points you at every caller. This is the same instinct as wrapping fetch calls in an API layer, applied to IPC.

Strategy 2: Generate the Types

The robust option is to generate TypeScript bindings from your Rust commands, so the two sides can't drift. Tools like tauri-specta read your command signatures and emit typed client functions — rename a Rust field and the generated TS changes, breaking your build at compile time instead of at runtime. For a serious app this is worth the setup; it turns the honest gap into a compiler-enforced contract.

Where to Go Next with Rust

That's the bridge: just-enough Rust, commands, serde, async, errors, and types. It's enough to build a real app. When you want the deep language — traits, lifetimes, generics, the iterator toolkit, fearless concurrency — that's a whole quest of its own at /cwk-quests/rust-quest. You've crossed the bridge; the rest of this quest builds on the far side.

Code

Strategy 1: a typed IPC layer in one module·ts
// src/lib/ipc.ts — the single place invoke is allowed to live.
import { invoke } from "@tauri-apps/api/core";

export interface Note {
  id: number;
  title: string;
  createdAt: string;
}

// Every command gets a typed wrapper. Components import these, never invoke.
export const getNote = (id: number) =>
  invoke<Note>("get_note", { id });

export const saveNote = (draft: Note) =>
  invoke<Note>("save_note", { draft });

// A component now reads: const note = await getNote(42);
// Rename a command? Fix it here once; TS flags every caller.

External links

Exercise

Refactor your frontend so no component calls invoke directly. Create src/lib/ipc.ts, move every invoke into a typed wrapper function there, and update components to import those. You've just built the seam that makes IPC changes safe — the same move Cinder made deliberately.
Hint
Export one function per command, each returning invoke<ReturnType>(name, args). Grep your components for invoke( afterward — there should be zero hits outside ipc.ts.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.