Skip to main content

Wasm Derive Macro

The Wasm derive macro is the foundation of TypeScript binding generation in Bomboni. It automatically generates:
  • TypeScript type declarations
  • wasm-bindgen integration code
  • Conversion traits (FromWasmAbi, IntoWasmAbi, etc.)
  • Error handling for WASM conversions

Basic Usage

use serde::{Deserialize, Serialize};
use bomboni_wasm::Wasm;

#[derive(Serialize, Deserialize, Wasm)]
pub struct User {
    pub name: String,
    pub age: i32,
}
Generated TypeScript:
export interface User {
  name: string;
  age: number;
}

Struct Attributes

The #[wasm(...)] attribute provides extensive customization options:

ABI Generation

Control which WASM ABI traits are generated:
#[derive(Serialize, Deserialize, Wasm)]
#[wasm(into_wasm_abi, from_wasm_abi)]
pub struct Task {
    pub id: u32,
    pub title: String,
}

// Or enable both at once
#[derive(Serialize, Deserialize, Wasm)]
#[wasm(wasm_abi)]
pub struct Item {
    pub value: String,
}
  • into_wasm_abi - Generate IntoWasmAbi for Rust → JavaScript conversion
  • from_wasm_abi - Generate FromWasmAbi for JavaScript → Rust conversion
  • wasm_abi - Enable both directions

Renaming

Customize type and field names in TypeScript:
#[derive(Serialize, Deserialize, Wasm)]
#[wasm(rename = "UserProfile")]
pub struct User {
    pub name: String,
}
Generated TypeScript:
export interface UserProfile {
  name: string;
}

Rename All Fields

Apply a naming convention to all fields:
#[derive(Serialize, Deserialize, Wasm)]
#[wasm(rename_all = "camelCase")]
pub struct UserSettings {
    pub user_name: String,
    pub email_address: String,
    pub is_active: bool,
}
Generated TypeScript:
export interface UserSettings {
  userName: string;
  emailAddress: string;
  isActive: boolean;
}
Supported rename rules:
  • camelCase - Convert to camelCase
  • PascalCase - Convert to PascalCase
  • snake_case - Convert to snake_case
  • SCREAMING_SNAKE_CASE - Convert to SCREAMING_SNAKE_CASE
  • kebab-case - Convert to kebab-case
  • SCREAMING-KEBAB-CASE - Convert to SCREAMING-KEBAB-CASE

Override Type

Replace the generated TypeScript type entirely:
#[derive(Serialize, Deserialize, Wasm)]
#[wasm(override_type = "Date")]
pub struct Timestamp {
    pub seconds: i64,
    pub nanos: i32,
}
Generated TypeScript:
export type Timestamp = Date;

Field Attributes

Rename Individual Fields

#[derive(Serialize, Deserialize, Wasm)]
pub struct User {
    #[wasm(rename = "userId")]
    pub id: u32,
    #[wasm(rename = "fullName")]
    pub name: String,
}
Generated TypeScript:
export interface User {
  userId: number;
  fullName: string;
}

Override Field Types

use chrono::{DateTime, Utc};

#[derive(Serialize, Deserialize, Wasm)]
pub struct Event {
    pub name: String,
    #[wasm(override_type = "Date")]
    pub created_at: DateTime<Utc>,
}
Generated TypeScript:
export interface Event {
  name: string;
  created_at: Date;
}

Always Some

Force optional fields to be non-optional in TypeScript:
#[derive(Serialize, Deserialize, Wasm)]
pub struct Config {
    #[serde(default)]
    #[wasm(always_some)]
    pub timeout: Option<u32>,
}
Generated TypeScript:
export interface Config {
  timeout: number;  // Not optional
}

Type Mapping

Basic Type Mapping

Bomboni automatically maps Rust types to TypeScript:

Numeric Types

#[derive(Serialize, Deserialize, Wasm)]
pub struct Numbers {
    pub byte: u8,
    pub short: i16,
    pub int: i32,
    pub long: i64,
    pub float: f32,
    pub double: f64,
}
Generated TypeScript:
export interface Numbers {
  byte: number;
  short: number;
  int: number;
  long: number;
  float: number;
  double: number;
}

BigInt Types (with js feature)

#[derive(Serialize, Deserialize, Wasm)]
pub struct LargeNumbers {
    pub big: i128,
    pub huge: u128,
}
Generated TypeScript:
export interface LargeNumbers {
  big: bigint;
  huge: bigint;
}

Collections

use std::collections::{HashMap, HashSet};

#[derive(Serialize, Deserialize, Wasm)]
pub struct Collections {
    pub list: Vec<String>,
    pub set: HashSet<i32>,
    pub map: HashMap<String, i32>,
}
Generated TypeScript:
export interface Collections {
  list: string[];
  set: number[];
  map: Map<string, number>;
}

Options

#[derive(Serialize, Deserialize, Wasm)]
pub struct OptionalFields {
    pub required: String,
    #[serde(default)]
    pub optional: Option<String>,
}
Generated TypeScript:
export interface OptionalFields {
  required: string;
  optional?: string | null;
}

Reference Type Mapping

Change how reference types are mapped:
#[derive(Serialize, Deserialize, Wasm)]
#[wasm(change_refs = [("OldType", "NewType")])]
pub struct Container {
    pub data: OldType,
}
Or apply to individual fields:
#[derive(Serialize, Deserialize, Wasm)]
pub struct Container {
    #[wasm(change_ref = "UserId")]
    pub user: String,
}

Wrapper Type Renaming

Rename protobuf wrapper types to primitive types:
#[derive(Serialize, Deserialize, Wasm)]
#[wasm(rename_wrapper)]
pub struct Message {
    pub int_value: Int32Value,
    pub string_value: StringValue,
    pub bool_value: BoolValue,
}
Generated TypeScript:
export interface Message {
  int_value: number;
  string_value: string;
  bool_value: boolean;
}
Supported wrapper types:
  • DoubleValuenumber
  • FloatValuenumber
  • Int32Valuenumber
  • UInt32Valuenumber
  • Int64Valuestring
  • UInt64Valuestring
  • BoolValueboolean
  • StringValuestring
  • BytesValueUint8Array or number[]

Enum Support

Externally Tagged Enums

#[derive(Serialize, Deserialize, Wasm)]
pub enum Status {
    Active,
    Inactive,
    Pending(String),
}
Generated TypeScript:
export type Status = {
  Active?: null;
  Inactive?: null;
  Pending?: null;
} | {
  Pending: string;
  Active?: null;
  Inactive?: null;
};

Internally Tagged Enums

#[derive(Serialize, Deserialize, Wasm)]
#[serde(tag = "type")]
pub enum Message {
    Text { content: String },
    Image { url: String, width: u32, height: u32 },
}
Generated TypeScript:
export type Message = {
  type: "Text";
  content: string;
} | {
  type: "Image";
  url: string;
  width: number;
  height: number;
};

Adjacently Tagged Enums

#[derive(Serialize, Deserialize, Wasm)]
#[serde(tag = "kind", content = "data")]
pub enum Response {
    Success(String),
    Error { code: i32, message: String },
}
Generated TypeScript:
export type Response = {
  kind: "Success";
  data: string;
} | {
  kind: "Error";
  data: {
    code: number;
    message: string;
  };
};

Enum Value Objects

Generate JavaScript enum-like objects:
#[derive(Serialize, Deserialize, Wasm)]
#[wasm(enum_value)]
pub enum Color {
    Red,
    Green,
    Blue,
}
Generated TypeScript:
export enum Color {
  RED = "Red",
  GREEN = "Green",
  BLUE = "Blue",
}

Tsify Compatibility

Bomboni’s Wasm derive is inspired by and compatible with tsify. The implementation in bomboni_wasm_derive/src/wasm.rs includes:
//! Based on [1] with extra features.
//!
//! [1]: https://github.com/madonoharu/tsify
Key differences and enhancements:
  • Additional proxy type support
  • Custom JsValue conversion options
  • Enum value object generation
  • Reference type mapping
  • Wrapper type renaming for protobuf types

Custom Crate Paths

When using Bomboni in a workspace or with custom module structure:
#[derive(Serialize, Deserialize, Wasm)]
#[wasm(
    wasm_bindgen_crate = "crate::wasm",
    js_sys_crate = "crate::js",
    bomboni_crate = "crate::internal::bomboni",
    bomboni_wasm_crate = "crate::internal::bomboni::wasm",
)]
pub struct CustomPath {
    pub field: String,
}

Complete Example

use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use bomboni_wasm::Wasm;
use std::collections::HashMap;

#[derive(Serialize, Deserialize, Wasm)]
#[wasm(rename_all = "camelCase", into_wasm_abi, from_wasm_abi)]
pub struct UserProfile {
    #[wasm(rename = "userId")]
    pub id: u32,
    
    pub user_name: String,
    pub email_address: String,
    
    #[serde(default)]
    pub avatar_url: Option<String>,
    
    #[wasm(override_type = "Date")]
    pub created_at: i64,
    
    pub tags: Vec<String>,
    pub metadata: HashMap<String, String>,
}

#[derive(Serialize, Deserialize, Wasm)]
#[serde(tag = "type")]
pub enum Notification {
    Message { from: String, text: String },
    Alert { level: String, message: String },
}

#[wasm_bindgen]
pub fn get_user_profile(id: u32) -> Result<UserProfile, JsValue> {
    // Implementation
    Ok(UserProfile {
        id,
        user_name: "alice".to_string(),
        email_address: "[email protected]".to_string(),
        avatar_url: None,
        created_at: 1234567890,
        tags: vec!["admin".to_string()],
        metadata: HashMap::new(),
    })
}
Generated TypeScript:
export interface UserProfile {
  userId: number;
  userName: string;
  emailAddress: string;
  avatarUrl?: string | null;
  createdAt: Date;
  tags: string[];
  metadata: Map<string, string>;
}

export type Notification = {
  type: "Message";
  from: string;
  text: string;
} | {
  type: "Alert";
  level: string;
  message: string;
};

export function get_user_profile(id: number): UserProfile;

Build docs developers (and LLMs) love