DEV Community

tengxgfyrz67s
tengxgfyrz67s

Posted on

TypeScript Interoperability in euv

Project Code:https://github.com/euv-dev/euv

euv is a Rust + WebAssembly frontend UI framework that compiles Rust code into WASM modules running in the browser. While the core UI logic is written in Rust, real-world projects often need to interoperate with the vast TypeScript/JavaScript ecosystem — from calling JavaScript libraries to exposing Rust functions to TypeScript code. This article explores how euv leverages wasm-bindgen, js-sys, and web-sys to enable seamless interoperability between Rust and TypeScript.

Table of Contents

  1. Understanding the WASM Interop Layer
  2. Calling JavaScript from Rust with js-sys
  3. Accessing Browser APIs with web-sys
  4. Exposing Rust Functions to TypeScript
  5. Working with JavaScript Promises and Async
  6. Practical Patterns for TS Interop
  7. Conclusion

Understanding the WASM Interop Layer

When euv compiles to WebAssembly, the resulting .wasm module communicates with JavaScript through a thin glue layer generated by wasm-bindgen. This tool automatically generates JavaScript wrapper functions that handle the marshaling of types across the Rust-JavaScript boundary.

The key crates involved in this interop are:

  • wasm-bindgen — Generates the JS glue code and provides the #[wasm_bindgen] attribute for exposing Rust functions to JavaScript
  • js-sys — Provides raw bindings to JavaScript's standard built-in objects (Object, Array, Promise, JSON, Math, etc.)
  • web-sys — Provides raw bindings to the browser's Web APIs (Window, Document, Storage, fetch, etc.)

In euv, all types from js-sys and web-sys are re-exported through use euv::*, so you never need to add these crates as direct dependencies. The framework handles the interop plumbing for you.

Calling JavaScript from Rust with js-sys

The js-sys crate provides bindings to JavaScript's core built-in objects. Through euv's re-exports, you can access these directly.

Working with JavaScript Objects

use euv::*;

// Create a JavaScript object
let obj: Object = Object::new();

// Set properties on the object
Reflect::set(&obj, &"name".into(), &"euv".into()).unwrap();

// Get properties from the object
let name: JsValue = Reflect::get(&obj, &"name".into()).unwrap();
let name_string: String = name.as_string().unwrap_or_default();
Enter fullscreen mode Exit fullscreen mode

Working with JavaScript Arrays

use euv::*;

// Create a JavaScript array
let arr: Array = Array::new();

// Push values to the array
arr.push(&"Rust".into());
arr.push(&"WASM".into());
arr.push(&"euv".into());

// Iterate over the array
arr.for_each(&mut |value: JsValue, _index: u32, _array: &Array| {
    web_sys::console::log_1(&value);
});

// Get array length
let length: u32 = arr.length();
Enter fullscreen mode Exit fullscreen mode

Using JSON

use euv::*;

// Parse a JSON string
let json_string: String = r#"{"name": "euv", "version": "0.1"}"#.to_string();
let json_value: JsValue = JSON::parse(&json_string).unwrap();

// Stringify a JavaScript object
let obj: Object = Object::new();
Reflect::set(&obj, &"framework".into(), &"euv".into()).unwrap();
let stringified: String = JSON::stringify(&obj).unwrap().as_string().unwrap_or_default();
Enter fullscreen mode Exit fullscreen mode

Using JavaScript Promises

use euv::*;

// Create a JavaScript promise that resolves after a delay
let promise: Promise = Promise::new(&mut |resolve: Function, _reject: Function| {
    let window: Window = window().expect("no global window exists");
    let closure: Closure<dyn Fn()> = Closure::new(move || {
        let _ = resolve.call0(&JsValue::NULL);
    });
    window.set_timeout_with_callback_and_timeout_and_arguments_0(
        closure.as_ref().unchecked_ref(),
        1000,
    ).expect("set_timeout failed");
    closure.forget();
});

// Convert to JsFuture and await it
let future: JsFuture = JsFuture::from(promise);
match future.await {
    Ok(value) => {
        web_sys::console::log_1(&"Promise resolved!".into());
    }
    Err(err) => {
        web_sys::console::log_1(&err);
    }
}
Enter fullscreen mode Exit fullscreen mode

Accessing Browser APIs with web-sys

The web-sys crate provides comprehensive bindings to browser APIs. euv re-exports these through use euv::* and pre-enables commonly used features in its Cargo.toml.

Window and Document Access

use euv::*;

let win: Window = window().expect("no global window exists");
let doc: Document = win.document().expect("should have a document");
Enter fullscreen mode Exit fullscreen mode

Navigation and Routing

use euv::*;

let win: Window = window().expect("no global window exists");
let location: Location = win.location();

// Get current hash route
let hash: String = location.hash().unwrap_or_default();

// Set hash route (page navigation)
let _ = location.set_hash("#/about");
Enter fullscreen mode Exit fullscreen mode

Local Storage

use euv::*;

let win: Window = window().expect("no global window exists");
let storage: Option<Storage> = win.local_storage().unwrap_or_default();

if let Some(storage) = storage {
    // Write
    let _ = storage.set_item("key", "value");
    // Read
    let value: Option<String> = storage.get_item("key").unwrap_or_default();
    // Delete
    let _ = storage.remove_item("key");
}
Enter fullscreen mode Exit fullscreen mode

Making HTTP Requests with fetch

use euv::*;

let loading: Signal<bool> = use_signal(|| false);
let loading_updater: Signal<bool> = loading;
let data: Signal<String> = use_signal(|| "".to_string());
let data_updater: Signal<String> = data;
let error: Signal<String> = use_signal(|| "".to_string());
let error_updater: Signal<String> = error;

button {
    onclick: move |_event: Event| {
        loading_updater.set(true);
        error_updater.set("".to_string());
        let data_updater_clone: Signal<String> = data_updater;
        let loading_updater_clone: Signal<bool> = loading_updater;
        let error_updater_clone: Signal<String> = error_updater;
        spawn_local(async move {
            let window: Window = window().expect("no global window exists");
            let promise: Promise = window.fetch_with_str("https://httpbin.org/get");
            let future: JsFuture = JsFuture::from(promise);
            match future.await {
                Ok(response) => {
                    let resp: Response = response.dyn_into().unwrap();
                    let json_promise: Promise = resp.json().unwrap();
                    let json_future: JsFuture = JsFuture::from(json_promise);
                    match json_future.await {
                        Ok(json) => {
                            let json_string: String = JSON::stringify(&json).unwrap().as_string().unwrap_or_default();
                            data_updater_clone.set(json_string);
                        }
                        Err(_) => {
                            error_updater_clone.set("Failed to parse JSON".to_string());
                        }
                    }
                }
                Err(_) => {
                    error_updater_clone.set("Network request failed".to_string());
                }
            }
            loading_updater_clone.set(false);
        });
    }
    "Fetch Data"
}
Enter fullscreen mode Exit fullscreen mode

In this example, spawn_local is euv's re-export of wasm_bindgen_futures::spawn_local, and JsFuture, JSON, Promise, Window, Response are all available through use euv::* without needing separate js-sys or web-sys dependencies.

Exposing Rust Functions to TypeScript

To make Rust functions callable from TypeScript, you use the #[wasm_bindgen] attribute. This generates the necessary JavaScript glue code.

Basic Function Export

use euv::*;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn main() {
    console_error_panic_hook::set_once();
    mount("#app", app);
}
Enter fullscreen mode Exit fullscreen mode

The #[wasm_bindgen] attribute on the main function tells wasm-bindgen to generate a JavaScript wrapper. This function can then be called from TypeScript:

import init, { main } from './pkg/my_euv_app.js';

await init();
main();
Enter fullscreen mode Exit fullscreen mode

Exporting Functions with Parameters

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}
Enter fullscreen mode Exit fullscreen mode

From TypeScript:

import { greet } from './pkg/my_euv_app.js';

const message = greet('World'); // "Hello, World!"
Enter fullscreen mode Exit fullscreen mode

Exporting Functions that Return JsValue

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn get_config() -> JsValue {
    let obj = js_sys::Object::new();
    js_sys::Reflect::set(&obj, &"name".into(), &"euv".into()).unwrap();
    js_sys::Reflect::set(&obj, &"version".into(), &"0.1".into()).unwrap();
    obj.into()
}
Enter fullscreen mode Exit fullscreen mode

Working with JavaScript Promises and Async

euv's spawn_local function (re-exported from wasm_bindgen_futures::spawn_local) is the key to running asynchronous Rust code in the WASM environment. It allows you to spawn Rust futures that integrate seamlessly with the JavaScript event loop.

Fetching Data Asynchronously

use euv::*;

spawn_local(async move {
    let window: Window = window().expect("no global window exists");
    let promise: Promise = window.fetch_with_str("https://httpbin.org/get");
    let future: JsFuture = JsFuture::from(promise);
    match future.await {
        Ok(response) => {
            let resp: Response = response.dyn_into().unwrap();
            let json_promise: Promise = resp.json().unwrap();
            let json_future: JsFuture = JsFuture::from(json_promise);
            match json_future.await {
                Ok(json) => {
                    let json_string: String = JSON::stringify(&json).unwrap().as_string().unwrap_or_default();
                    data_updater_clone.set(json_string);
                }
                Err(_) => {
                    error_updater_clone.set("Failed to parse JSON".to_string());
                }
            }
        }
        Err(_) => {
            error_updater_clone.set("Network request failed".to_string());
        }
    }
    loading_updater_clone.set(false);
});
Enter fullscreen mode Exit fullscreen mode

[!tip]

All browser APIs are accessed through web-sys crate — euv pre-enables common web-sys features (such as Window, Document, Storage, Clipboard, Navigator, FileReader, FileList, IntersectionObserver, etc.) in its Cargo.toml, and re-exports them through use euv::*. You never need to manually add web-sys as a dependency.

Using spawn_local with Signals

A common pattern is to use spawn_local to perform async operations and then update reactive signals when the operation completes:

let loading: Signal<bool> = use_signal(|| false);
let loading_updater: Signal<bool> = loading;
let data: Signal<String> = use_signal(|| "".to_string());
let data_updater: Signal<String> = data;
let error: Signal<String> = use_signal(|| "".to_string());
let error_updater: Signal<String> = error;

button {
    onclick: move |_event: Event| {
        loading_updater.set(true);
        error_updater.set("".to_string());
        let data_updater_clone: Signal<String> = data_updater;
        let loading_updater_clone: Signal<bool> = loading_updater;
        let error_updater_clone: Signal<String> = error_updater;
        spawn_local(async move {
            // ... async operations ...
            data_updater_clone.set("result".to_string());
            loading_updater_clone.set(false);
        });
    }
    "Fetch Data"
}
Enter fullscreen mode Exit fullscreen mode

[!warning]

Signal clones used in async closures must be cloned outside the closure and then moved in, otherwise borrow conflicts will occur.

Practical Patterns for TS Interop

Pattern 1: Calling JavaScript Libraries

When you need to use a JavaScript library (e.g., a charting library or date picker), you can create Rust bindings using #[wasm_bindgen] extern blocks:

use wasm_bindgen::prelude::*;

#[wasm_bindgen(module = "some-js-library")]
extern "C" {
    #[wasm_bindgen(js_name = "someFunction")]
    pub fn some_function(input: &str) -> JsValue;
}
Enter fullscreen mode Exit fullscreen mode

Then call it from Rust:

let result: JsValue = some_function("input");
let result_string: String = result.as_string().unwrap_or_default();
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Passing Callbacks to JavaScript

use wasm_bindgen::prelude::*;

let callback: Closure<dyn FnMut(JsValue)> = Closure::new(move |value: JsValue| {
    let value_string: String = value.as_string().unwrap_or_default();
    data_updater.set(value_string);
});

// Pass the callback to a JavaScript function
some_js_function(callback.as_ref().unchecked_ref());

// Important: forget the closure to prevent it from being dropped
// (only if you want it to persist beyond the current scope)
// callback.forget();
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Using web-sys with Event Handlers

use euv::*;

html! {
    input {
        r#type: "text"
        placeholder: "Enter text"
        oninput: move |event: Event| {
            if let Some(target) = event.target()
                && let Ok(input) = target.clone().dyn_into::<HtmlInputElement>() {
                    name_signal.set(input.value());
                }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, dyn_into::<HtmlInputElement>() comes from wasm_bindgen::JsCast, which is re-exported through euv. The HtmlInputElement type is from web-sys, also re-exported through use euv::*.

Pattern 4: Combining Browser APIs with Reactive Signals

let tab: Signal<String> = use_signal(|| {
    let win: Window = window().expect("no global window exists");
    let location: Location = win.location();
    let hash: String = location.hash().unwrap_or_default();
    if hash == "#/settings" {
        "settings".to_string()
    } else {
        "info".to_string()
    }
});

html! {
    div {
        match { tab.get().as_str() } {
            "info" => { div { "Information content" } }
            _ => { div { "Settings content" } }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This pattern derives the initial signal value from the browser's URL hash, ensuring the UI state matches the URL on page load.

Pattern 5: Persisting State to Local Storage

let name: Signal<String> = use_signal(|| {
    let win: Window = window().expect("no global window exists");
    let storage: Option<Storage> = win.local_storage().unwrap_or_default();
    if let Some(storage) = storage {
        storage.get_item("user_name").unwrap_or_default()
    } else {
        "".to_string()
    }
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

euv's TypeScript interoperability is built on the solid foundation of wasm-bindgen, js-sys, and web-sys. Through euv's re-export system (use euv::*), you get seamless access to:

  • JavaScript built-in objects (Object, Array, Promise, JSON, Math, etc.) via js-sys
  • Browser APIs (Window, Document, Location, Storage, fetch, Navigator, etc.) via web-sys
  • Async operations via spawn_local (re-exported from wasm_bindgen_futures)
  • Type casting via JsCast trait (dyn_ref, dyn_into)
  • Function export via #[wasm_bindgen] attribute

The key takeaway is that euv handles all the interop plumbing for you. You never need to add js-sys or web-sys as direct dependencies — everything is available through use euv::*. This allows you to focus on writing your application logic in Rust while still having full access to the JavaScript ecosystem when needed.

When building euv applications that need TypeScript interop, remember to:

  1. Use #[wasm_bindgen] to expose Rust functions to JavaScript
  2. Use spawn_local for async operations that need to interact with JavaScript APIs
  3. Clone signals before moving them into async closures
  4. Use dyn_ref and dyn_into for type-safe event handling
  5. Leverage euv's re-exports to avoid managing js-sys and web-sys dependencies directly

Project Code:https://github.com/euv-dev/euv

Top comments (0)