# ๐ Tsain: Chain `TS` & `RS` in _Fast_ and _Secure_ Way
Tsain carries Rust types across the `wasm-bindgen` boundary as positional **arrays** instead of named objects. One idea, two payoffs:
- **Fast** โ dropping field and variant names makes ser/de faster than `serde-wasm-bindgen`.
- **Secure** โ names never appear in the emitted JS, so (with mangling) your interface stays hidden.
It is meant for Rust โ WASM projects that already use `wasm-bindgen`.
Concretely, Tsain does two things, both built on that array format:
1. **Serde** between `rust` & `JsValue`.
It builds on `serde-wasm-bindgen`'s serde approach, but `tsain` serializes structs and enums into an Array format. This prevents field and variant names from being revealed in the resulting JS file.
2. **Export** a TypeScript file holding tsain's exported types and the factory functions that construct them.
It follows `Tsify`'s format, but structs, enums, and the like use tsain's derived Array format.
These are not two separate features โ they are two views of **one** thing. A Rust type has a single canonical array shape; ser/de reads and writes that shape at runtime, and the TS export describes the same shape at the type level so JS can build and read it safely.
```text
โโ runtime value: e.g. [1, 2] โ ยง1 (serde)
Point { x, y } โโโบ [number, number] โโค
โโ TypeScript: type Point = [number, number]
+ Point_() factory & getters โ ยง2 (export)
```
## Getting Started
```sh
cargo add tsain serde wasm-bindgen
```
```rust
// Rust
use tsain::*;
use serde::{Serialize, Deserialize};
use wasm_bindgen::prelude::*;
#[derive(Tsain, Serialize, Deserialize)]
pub struct Point {
x: i32,
y: i32,
}
// Pass tsain types across the wasm boundary with `Ts<T>`.
#[wasm_bindgen]
pub fn make_point() -> TsainResult<Ts<Point>> {
Ts::from_rust(&Point { x: 1, y: 2 })
}
#[wasm_bindgen]
pub fn use_point(point: Ts<Point>) -> TsainResult<()> {
let _point: Point = point.to_rust()?;
Ok(())
}
// Emit the matching TypeScript. Run it with `cargo test`
#[test]
fn export_ts() {
tsain::TsScript::export("./Tsain.ts");
}
```
```ts
// Exported TS file
// Point Type
export type Point = [number, number] & { readonly __brand: "Point" };
// Point Constructor
export const Point_ = (x: number, y: number) => [x, y] as Point;
// Point Getters: Point_x, Point_y (...)
```
## 1. At runtime โ serde between `rust` & `JsValue`
This is the canonical array shape seen at runtime: a value is written to / read from its array form.
### Rust to JsValue: `tsain::to_value`
```rust
use tsain::*;
use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
pub struct Point {
x: i32,
y: i32,
#[serde(skip)] // This field will be skipped
z: i32,
}
let point = Point { x: 10i32, y: 20i32, z: 30i32 };
let js_value: JsValue = tsain::to_value(&point).unwrap();
```
### JsValue to Rust: `tsain::from_value`
```rust
let js_value: JsValue = js_sys::Array::of2(&10i32.into(), &20i32.into()).into();
let point: Point = tsain::from_value(js_value).unwrap();
```
### Crossing the `#[wasm_bindgen]` boundary: `Ts<T>`
`Ts<T>` is the runtime conversion above, wrapped for the wasm-bindgen ABI: it derives `IntoWasmAbi` and `FromWasmAbi` so tsain types can be passed directly to and from `#[wasm_bindgen]` functions, instead of manually calling `to_value`/`from_value` on a `JsValue`. This is a straight import of [Tsify](https://github.com/madonoharu/tsify)'s `Ts<T>`.
```rust
use tsain::*;
use wasm_bindgen::prelude::*;
#[derive(Tsain, Serialize, Deserialize)]
pub struct Point {
x: i32,
y: i32,
}
#[wasm_bindgen]
pub fn into_js() -> TsainResult<Ts<Point>> {
let point = Point { x: 0, y: 0 };
let x = Ts::from_rust(&point)?;
Ok(x)
}
#[wasm_bindgen]
pub fn from_js(point: Ts<Point>) -> TsainResult<()> {
let _point: Point = point.to_rust()?;
Ok(())
}
```
### Conversion formats
- Struct <=> [...struct's fields]
```rust
struct Example { a: String, b: Vec<Point> } ```
- Enum <=> [variant-number, [...variant's fields]]
```rust
enum Example {
First, Second(String, Vec<Point>), Third { a: u32, b: i32, } }
```
A unit variant is serialized from Rust as `[0]`, while the exported TS constructor builds `[0, []]`; deserialization accepts both, since the payload of a unit variant is ignored.
- Other types: follow [serde-wasm-bindgen](https://crates.io/crates/serde-wasm-bindgen)
## 2. At the type level โ export TS types and factory functions
This is the same array shape described in TypeScript, so JS code can build and read it without ever touching field names.
The `Tsain` proc macro builds a TS script of the Tsain-derived types and the factory functions that construct them.
Because `tsain` uses the "Array format" by default, without field or variant names, users need a factory function to construct the TS type.
Unlike `Tsify`, `Tsain` does not insert types into the `.d.ts` file; instead, it encourages users to use `tsain::TsScript` to explicitly write a `.ts` file.
(This is because `tsain` exports not only types but also some actual functions: currently it is not possible to insert functions into wasm-pack built files.)
`tsain` also exports each TS type with a TypeScript "brand pattern" so they can be distinguished from one another. (For example, this makes it possible to distinguish different enum types that actually share the same JS type.) The brand pattern can be controlled with attributes. Only struct and enum-variant export types have a brand pattern; the enum type itself does not (and should not).
### Use `tsain::TsScript` to write TS file
- When using it inside a lib crate, use `cargo test` to run the script building.
```rust
// lib.rs
#[test]
fn export_ts() {
// Option1: Export all types with Tsain macro derived
tsain::TsScript::export("./Tsain.ts");
// Option2: Export listed types
tsain::TsScript::new()
.with_comment("export const X: number = 10;\n\n") // comments
.with_exclusive(vec!["Point"]) // exclusive list: only includes these types when the list is not empty.
.with_exclude(vec!["Vessel"]) // exclude any types in this list
.export_script("./Tsain2.ts");
}
```
### โ
Example1: Struct
```rust
use tsain::Tsain;
use serde::{Serialize, Deserialize};
#[derive(Tsain, Serialize, Deserialize)]
struct Point {
x: i32,
y: i32,
#[serde(skip)] // This field will be skipped
z: i32,
}
```
โก๏ธ Declare TS (the same `Point` from ยง1, now described at the type level):
```ts
// 1. Type: by default, tsain adds a brand pattern (use `skip_brand` attribute to omit it)
export type Point = [number, number] & { readonly __brand: "Point" };
// 2. Factory function: named as `{TypeName}_`
export const Point_ = (x: number, y: number) => [x, y] as Point;
// 3. Getters
export const Point_x_id: number = 0;
export const Point_x = (v: Point): number => v[0];
export const Point_y_id: number = 1;
export const Point_y = (v: Point): number => v[1];
```
### โ
Example2: Enum
```rust
use tsain::Tsain;
#[derive(Tsain, Serialize, Deserialize)]
enum Vessel {
// Internal types are supposed to impl Tsain too
Origin(Point),
OilTanker,
#[serde(skip)] // This variant will be skipped
WarShip,
Boat {
name: String,
capacity: Vec<usize>,
#[serde(skip)] // This field will be skipped
uid: String,
color: String,
},
// override variant's TS name
#[tsain(name = Pirogue)]
Canoe { name: String },
}
```
โก๏ธ Declare TS:
```ts
// 1. Enum Union Type
// 2. Var Ids
export const VesselOriginId: number = 0;
export const VesselOilTankerId: number = 1;
export const VesselBoatId: number = 3;
export const PirogueId: number = 4;
// 3. As Each Var
export const asVesselOilTanker = (e: Vessel): VesselOilTanker | undefined =>
e[0] == 1 ? (e as VesselOilTanker) : undefined;
export const asVesselBoat = (e: Vessel): VesselBoat | undefined =>
e[0] == 3 ? (e as VesselBoat) : undefined;
export const asPirogue = (e: Vessel): Pirogue | undefined =>
e[0] == 4 ? (e as Pirogue) : undefined;
// 3. Each Vars
// Variant 0: VesselOrigin
// 1. Type
export type VesselOrigin = [0, [Point]] & { readonly __brand: "VesselOrigin" };
// 2. Constructor
export const VesselOrigin_ = (f0: Point) => [0, [f0]] as VesselOrigin;
// 3. Getters
export const VesselOrigin_f0_id: number = 0;
export const VesselOrigin_f0 = (v: VesselOrigin): Point => v[1][0];
// Variant 1: VesselOilTanker
// 1. Type
export type VesselOilTanker = [1, []] & { readonly __brand: "VesselOilTanker" };
// 2. Constructor
export const VesselOilTanker_ = () => [1, []] as VesselOilTanker;
// Variant 3: VesselBoat
// 1. Type
export type VesselBoat = [3, [string, number[]]] & {
readonly __brand: "VesselBoat";
};
// 2. Constructor
export const VesselBoat_ = (name: string, capacity: number[]) =>
[3, [name, capacity]] as VesselBoat;
// 3. Getters
export const VesselBoat_name_id: number = 0;
export const VesselBoat_name = (v: VesselBoat): string => v[1][0];
export const VesselBoat_capacity_id: number = 1;
export const VesselBoat_capacity = (v: VesselBoat): number[] => v[1][1];
// Variant 4: Pirogue โ name overridden via #[tsain(name = Pirogue)]
// 1. Type
export type Pirogue = [4, [string]] & { readonly __brand: "Pirogue" };
// 2. Constructor
export const Pirogue_ = (name: string) => [4, [name]] as Pirogue;
// 3. Getters
export const Pirogue_name_id: number = 0;
export const Pirogue_name = (v: Pirogue): string => v[1][0];
```
### โ
Example3: Generic Types
`#[derive(Tsain)]` works directly on generic structs and enums:
```rust
use tsain::*;
use serde::{Serialize, Deserialize};
#[derive(Tsain, Serialize, Deserialize)]
struct Page<T> {
items: Vec<T>,
total: u32,
}
#[derive(Tsain, Serialize, Deserialize)]
enum Status<T> {
Ok(T),
Err(String),
}
```
โก๏ธ Declare TS:
```ts
// # Struct Page
export type Page<T> = [T[], number] & { readonly __brand: "Page" };
export const Page_ = <T>(items: T[], total: number) => [items, total] as Page<T>;
// (...)
// # Enum Status
export type StatusOk<T> = [0, [T]] & { readonly __brand: "StatusOk" };
export const StatusOk_ = <T>(f0: T) => [0, [f0]] as StatusOk<T>;
export type StatusErr = [1, [string]] & { readonly __brand: "StatusErr" };
export const StatusErr_ = (f0: string) => [1, [f0]] as StatusErr;
```
### Use `tsain!` macro for Type Alias
`tsain!` exports **type aliases** โ plain, generic, or pointing to a generic type. Multiple aliases can go in one block:
```rust
tsain! {
// plain tuple alias
pub type Cup = (String, u32);
// generic alias
pub type Pair<A, B> = (A, Vec<B>);
// concrete alias pointing to a generic type
pub type PageOfStatus = Page<Status<u32>>;
}
```
โก๏ธ Declare TS:
```ts
export type Cup = [string, number];
export type Pair<A, B> = [A, B[]];
export type PageOfStatus = Page<Status<number>>;
```
### Attributes of `#[tsain(..)]`
- `#[tsain(name = OtherName)]`
Override export type name
- `#[tsain(skip_brand)]`
Omit a default brand pattern of `__brand`.
Works only for struct or enum-variant.
- `#[tsain(brand(ret = Vec<u32>, msg = "Message"))]`
Add extra brand patterns, allows Type or string-literal as values.
Above attribute will add: `{ readonly ret: number[], readonly msg: "Message" }`
Works only for struct or enum-variant.
## More Tips
### Use `#[serde(expecting = "x")]` when deriving Serde
This will diminish wasm's data section messages a lot!
### Why `tsain` is Fast & Secure ๐
Tsain reuses `serde-wasm-bindgen` for primitive types, so in a sense it is a version of serde that combines the philosophies of `serde-wasm-bindgen` and `postcard`.
- **Fast** โ bench tests show Tsain is much faster than `serde-wasm-bindgen` at ser/de, thanks to omitting field names when encoding/decoding. For primitive types without field names, performance is basically equal to `serde-wasm-bindgen`.
- **Secure** โ with proper mangling, the original names of types, functions, and properties are not exposed in the emitted JS. This was the very first motivation for developing Tsain.