I’ve been looking at integrating a React front-end with a Rust API server and one of the ongoing annoyances I encountered was keeping API types in sync with each other. I found a couple of options, the smallest and simplest one I liked was tsync.

I try to keep my dependency lists small, and this crate is a bit of an odd duck as it is only really doing work as a build dependency. If you look at the macro definition it doesn’t do anthing to the TokenStream its passed, making it a no-op attribute. It’s just used during this special pass done during the build step.

Since its a no-op during regular compilation, it’d be nice if the tsync code wasn’t compiled or linked in to the final application. The nature of attribute macros means they cause an error without the definition, and since Rust requires attribute macros be defined in a special type of crate they tend to be fully decoupled from the main crate which is also the case here.

The workaround uses the macro crate directly in normal builds while only using the full version during the build step. First we need to add both crates as direct dependencies:

cargo add --build tsync@^1
cargo add tsync-macro@^0.1

The versions are correct, the tsync-macro crate is independently versioned and hasn’t been bumped.

Now during builds we want our source scanned for the attribute, and a typescript file generated with all the appropriate types. You can create or edit an existing build.rs file with the following content to do so:

use std::path::PathBuf;

const TYPESCRIPT_OUTPUT: &str = "frontend/src/rust_types.d.ts";

fn generate_typescript_definitions() {
    let dir = env!("CARGO_MANIFEST_DIR");

    let inputs = vec![PathBuf::from(dir)];
    let output = PathBuf::from_iter([dir, TYPESCRIPT_OUTPUT]);

    tsync::generate_typescript_defs(inputs, output, false);
}

fn main() {
    generate_typescript_definitions();
}

My TypeScript/React frontend project lives inside my Rust project in a directory called “frontend”. If you’d like the file to be saved elsewhere, edit the value of the TYPESCRIPT_OUTPUT variable.

Using the attribute is pretty straight-forward but slightly different than what the crate’s documentation tell you to do:

use tsync_macro::tsync;

#[tsync]
pub type Timestamp = usize;

Or just inline:

#[tsync_macro::tsync]
type Timestamp = usize;

Run a build and you should see the file frontend/src/rust_types.d.ts get created with the following contents:

/* This file is generated and managed by tsync */

type Timestamp = number

Voila! Effortless type synchronization.