typeship is a small Rust library for assembling generated TypeScript API
surfaces from Rust-owned types and command metadata.
The crate is deliberately a facade, not a reflection engine. Per-type generators
such as ts-rs, specta, typeshare, or schemars can own the hard problem of
reading Rust types. typeship owns the assembly layer:
- deterministic generated-file headers;
- exported TypeScript declarations;
- typed command wrappers, currently for Tauri
invokeand a genericrequest; - a drift check for CI.
use typeship::ir::{Decl, Field, TsType};
use typeship::{Arg, Bridge, Command};
let profile = Decl::interface(
"ConnectionProfile",
[
Field::rust("id", TsType::string()),
Field::rust("host", TsType::string()).optional(),
],
);
let ts = Bridge::tauri()
.decl(&profile)
.command(
Command::new("db_connect", "ConnectionInfo")
.arg(Arg::new("profile", TsType::named("ConnectionProfile"))),
)
.render();
assert!(ts.contents.contains(
"export function dbConnect(profile: ConnectionProfile): Promise<ConnectionInfo>"
));crates/typeship— the core facade. Zero third-party dependencies. IR, renderer, command wrappers, drift check, and a tinyclidriver.crates/typeship-ts-rs— thets-rsbackend adapter.decl::<T>()lowers a#[derive(TS)]type into a typeship declaration. Aspecta/schemarsadapter could sit alongside it.samples/basic-ir— a transport-agnostic project operations API generated from hand-builttypeshipIR, including project filters, milestone reports, bulk status updates, audit events, inline objects, records, optional fields, nullable values, andbigintcounters.samples/tauri-ts-rs— a Tauri-style desktop data-workbench API generated fromts-rsderives plus command metadata. It covers connection profiles, optional read-only capabilities, environment grouping, query execution, import preview, saved dashboard layouts, dashboard widgets, filters, metric snapshots, and export commands.
typeship is not tied to irodori-table. That app is the first real boundary
the crate was checked against, but the core API is backend- and transport-light:
- use
Bridge::tauri()for Tauriinvoke<T>wrappers; - use
Bridge::fetch()for a genericrequest<T>(command, payload)client; - feed declarations from
typeship-ts-rs, another future adapter, or hand-builtDecl/TsTypevalues.
Keep product features in the application that owns them. For example, BI views,
ERD layout, query editors, and sidebar placement belong in irodori-table;
typeship should stay focused on generated Rust/TypeScript contracts, command
wrappers, and drift checks that other apps can reuse. It can model reusable
contract concepts such as readOnly / writePolicy, but it should not decide
how an application enforces those policies.
Regenerate the committed sample bindings:
npm run samples:writeCheck that the committed sample bindings are still up to date:
npm run samples:checkThe generated files live at:
samples/basic-ir/generated/api.tssamples/tauri-ts-rs/generated/api.ts
typeship::cli::run turns an assembled Bridge into a generator with write
and check verbs — check exits non-zero when the committed file has drifted:
fn main() -> std::process::ExitCode {
let bridge = build_bridge();
typeship::cli::run(&bridge, "src/generated/api.ts")
}Both verbs refuse to run (exit 2) when the bridge would render TypeScript that
does not compile — a duplicate identifier (two declarations sharing a name, or
two fields colliding through the naming isomorphism, where col_1 and col1
both become col1), or a name that cannot bind (kebab-case, a reserved word
such as function, or an empty one). Bridge::try_render exposes the same check
programmatically; Bridge::defects returns the list without rendering.
Member keys are the deliberate exception: a wire key is whatever serde puts in
the JSON, so #[serde(rename = "kebab-case")] renders as "kebab-case": T
rather than being rejected.
See the end-to-end example (ts-rs types → assembly → CLI):
cargo run -p typeship-ts-rs --example generate -- write /tmp/api.ts
cargo run -p typeship-ts-rs --example generate -- check /tmp/api.tsThe MVP was shaped by the irodori-table desktop boundary, while keeping the
surface reusable for other Rust + TypeScript applications:
- closed string-literal unions for Rust enums;
- interfaces for command payload structs;
snake_caseRust names rendered ascamelCaseTypeScript names;- optional object fields for serde shapes that may be absent;
- typed Tauri command wrappers (
invoke<T>); - byte-for-byte drift checking against committed generated files;
- pre-rendered declarations from a backend (ts-rs today) assembled verbatim.
npm run check
cargo package -p typeshipnpm run check runs formatting, all workspace tests, clippy, the committed
sample drift checks, and tsc --strict over every generated file and fixture
(npm run ts:check) — a codegen tool should prove its own output compiles.
cargo package -p typeship is a useful packaging smoke test because the core
crate manifest points at this README. The adapter crate is verified by the
release workflow's cargo publish -p typeship-ts-rs step after the matching
core crate version has reached crates.io.
Releases follow the same tag-push flow as irodori-table:
npm run release:patch
# or: npm run release:minor / npm run release:major
# or: node tools/release.mjs 0.2.1The release helper requires a clean worktree, bumps both crate versions plus the
typeship-ts-rs dependency on typeship, refreshes Cargo.lock, commits
chore: release vX.Y.Z, creates an annotated vX.Y.Z tag, and pushes
main --follow-tags.
Pushing the tag triggers .github/workflows/release.yml, which validates the tag
against the crate manifests and publishes typeship followed by
typeship-ts-rs to crates.io. The workflow expects CARGO_REGISTRY_TOKEN to be
configured in the GitHub repository secrets.
0BSD. You can use, copy, modify, and distribute this project for almost any purpose.
Irodori-authored code in this repository is available under 0BSD unless a file
says otherwise. See LICENSE.
typeship generates TypeScript bindings from Rust APIs, but generated code
still needs review before publishing or wiring into release checks. For the
broader Irodori product disclaimer, see
https://irodori-table.github.io/irodori-docs/disclaimer.html.