Type safety
Absolute type safetyThe same type system that makes Rust safe makes idealyst apps hard to get wrong. The function signature is the contract — across the network, across the component boundary, across a theme switch. Whole categories of UI bug stop being runtime surprises and start being compile errors.
The signature is the contractEverything you pass across a boundary is typed, and the compiler enforces it on both sides of that boundary. Component props are a real struct, not a stringly-typed bag — pass the wrong type, misspell a field, or omit a required one and the build fails with a precise message.The same idea scales up to the network. A server function's signature is the wire contract: the client call site and the server handler are generated from one declaration, so a request and its handler can never disagree about argument or return shape. You don't maintain a client API, a server API, and a DTO crate in lockstep — there's one source of truth and it's type-checked.
// A component's props are a typed struct. The compiler checks
// every call site against it — no untyped prop bags.
ui! { button(label = "Save".to_string(), on_click = on_save) }

// A server function's signature is the wire contract. The same
// types are checked on the client (the RPC stub) and the server
// (the handler) — they cannot drift out of sync.
#[server]
async fn save_todo(input: NewTodo) -> Result<Todo, ServerError> { ... }
Invalid states can't compileUI state modeled as a bag of optional flags admits combinations that should never happen — loading and loaded at once, data and error together. Each of those is a latent bug waiting for the wrong sequence of events.Modeled as a sum type, the state is exactly one of its variants. "Loading and loaded simultaneously" isn't a bug you guard against; it's a value the type system won't let you construct. The impossible states are gone before you write a single guard.
// In a dynamically-typed world, every combination is constructible:
{ loading: true,  data: result, error: "oops" }  // ...valid?!

// With a sum type, the nonsense states simply don't exist:
enum FetchState<T> {
    Idle,
    Loading,
    Loaded(T),
    Error(String),
}
Exhaustiveness, codebase-wideA `match` over a state enum must cover every variant. That turns rendering into a checked switch: the compiler guarantees you handled idle, loading, loaded, and error before the code builds.The guarantee holds as the code evolves. Add a new variant six months later and every `match` site across the entire codebase that doesn't handle it is a compile error — a worklist the compiler hands you for free. The class of bug where you add a state and forget to render it somewhere doesn't survive `cargo build`.
let view = match fetch_state.get() {
    FetchState::Idle       => idle_view(),
    FetchState::Loading    => spinner(),
    FetchState::Loaded(d)  => results_view(d),
    FetchState::Error(msg) => error_view(msg),
};

// Add `FetchState::Cached(T)` later, and EVERY match over
// FetchState becomes a compile error until you handle it.
Refs you can't misuseA `Ref<H>` to a node's handle doesn't expose the handle directly. The read API is a closure the framework runs only if the node is mounted right now — the borrow can't escape it.That shape is enforced at the type level, so the entire family of "used a ref after the component unmounted" crashes is unreachable. You can't accidentally call a method on a handle that's been torn down, because you were never able to hold onto it in the first place.
// You never hold a raw handle. You read it through a closure,
// and only while the node is actually mounted:
btn_ref.with(|handle| handle.focus());

// Returns Option<R> — None when the button isn't mounted.
// There's no way to stash a handle and call .focus() later,
// after the component might already be gone.
Styles and themes are typedStyling goes through `stylesheet!`, which gives each style a typed surface: variants and states are enums, not class-name strings you hope match something. Select the wrong variant and it's a compile error, not a silently-missing rule at runtime.Theme tokens are typed too. A token resolves to a concrete `Color`, `Length`, or scalar, and reading one subscribes the surrounding reactive scope so a theme switch re-resolves it automatically — no untyped CSS-variable lookups that fail quietly when a name drifts.
// Variants and states are typed axes on the stylesheet,
// not magic strings the compiler can't see.
let style = NavLink().active(derived(move || {
        if is_current.get() { NavLinkActive::On } else { NavLinkActive::Off }
    }));
Where to go from hereThese guarantees are the practical payoff of the language's shape. Why Rust makes the deeper argument — why expressions, pattern matching, ownership, and a real macro system fit UI authoring. The Server functions page shows the type contract stretched across the network.Read → Why RustThe signature as a wire contract →
On this page
The signature is the contract
Invalid states can't compile
Exhaustiveness, codebase-wide
Refs you can't misuse
Styles and themes are typed
Where to go from here
IdealystOne codebase, native everywhere.
© Idealyst 2026