Salta ai contenuti

Rust per sviluppatori JavaScript e TypeScript

Se scrivi JavaScript o TypeScript, conosci Rust più di quanto pensi. Rust ha closure che funzionano in modo molto simile alle arrow function, async/await, metodi degli iteratori con nomi familiari come map, filter e find, la destrutturazione, e un solo strumento da riga di comando (Cargo) che fa quello che fa npm. Se usi TypeScript, sei già abituato a un compilatore che trova gli errori prima che il codice parta. Il compilatore di Rust fa la stessa cosa, solo in modo più severo.

Allora cosa c’è di davvero nuovo? Tre cose. Rust non ha garbage collector: ogni valore ha un solo proprietario, e il compilatore controlla l’ownership (possesso) e il prestito (borrowing) prima che il programma parta. Rust non ha undefined né null: un valore che potrebbe mancare ha il tipo Option. E Rust non ha eccezioni: una funzione che può fallire restituisce un Result. È su queste tre idee che passerai la maggior parte del tempo di studio.

Perché gli sviluppatori JavaScript imparano Rust? Di solito per uno di questi motivi: vuoi un backend web o uno strumento da riga di comando veloce che si distribuisce come un unico file, vuoi accelerare un punto critico nel browser con WebAssembly, vuoi scrivere un addon nativo per Node.js, o sei curioso degli strumenti che usi già. SWC, Biome, Oxc, Rolldown, Rspack, Turbopack, Lightning CSS e Deno sono tutti scritti in Rust.

Questa pagina mette in corrispondenza quello che conosci di JavaScript e TypeScript con Rust, così puoi dedicare il tuo tempo alle parti davvero nuove. Se invece cerchi un confronto generale con C++, Go e Python, guarda Rust vs C++, Go e Python.

Concetti del linguaggio: da JavaScript e TypeScript a Rust

Sezione intitolata “Concetti del linguaggio: da JavaScript e TypeScript a Rust”

Usa questa tabella come un frasario. L’ultima colonna rimanda alla lezione di questo sito che spiega il lato Rust.

JavaScript / TypeScript Rust Per saperne di più
let e const let (non si può cambiare) e let mut (si può cambiare). Un oggetto const di JS si può comunque modificare all’interno; un valore let di Rust no Variabili
number Scegli tu una dimensione: i32, i64, u32, usize, f64, … Interi e float sono tipi diversi Tipi di dati
bigint i128 / u128, o il crate num-bigint per qualsiasi dimensione Tipi di dati
string String (posseduta, può crescere) e &str (una vista presa in prestito su un testo) Stringhe
boolean bool Booleani
Template literal format!("Hi {name}") e println!("Hi {name}") Output
any, unknown Niente any. Usa i generici, un enum, o serde_json::Value per JSON di forma sconosciuta Generici, JSON con Serde
interface o type che descrive la forma di un oggetto struct Struct
interface che descrive dei metodi trait Trait
Tipizzazione strutturale (stessa forma = stesso tipo) Tipizzazione nominale: due struct con gli stessi campi restano tipi diversi Struct
Tipi unione e unioni discriminate enum, in cui ogni variante può portare i propri dati Enum
Tipi letterali stringa come "small" o "large" Un enum con varianti semplici: enum Size { Small, Large } Enum
undefined, null, proprietà opzionale x?: T Option<T>: o Some(valore) o None Option
?. e ?? ? (dentro una funzione che restituisce Option), .map(...), .and_then(...), .unwrap_or(...) Option
Asserzione non-null x! .unwrap() o .expect("motivo"), che fermano il programma se il valore è None Panic
throw, try/catch, Promise rifiutate Result<T, E> + l’operatore ? Result
class MyError extends Error Un tuo enum di errore (spesso con thiserror); anyhow nelle applicazioni Errori personalizzati, anyhow e thiserror
class con campi e metodi struct per i dati + un blocco impl per i metodi Struct
extends (ereditarietà) Niente ereditarietà. Usa la composizione (una struct dentro una struct) + trait con metodi predefiniti Trait, Design pattern
class Dog implements Animal impl Animal for Dog Trait
Un array di classi diverse che condividono un’interfaccia Vec<Box<dyn Animal>> (un trait object) Trait object
Generici come T extends HasId Generici con vincoli di trait (trait bound): T: HasId Generici
Arrow function e callback Closure, descritte dai trait Fn, FnMut, FnOnce Closure, Fn, FnMut e FnOnce
this Un parametro esplicito self, &self o &mut self. Nessuna sorpresa su cosa indichi this Struct
Destrutturazione const { x, y } = point Pattern: let Point { x, y } = point; (anche nei match e nei parametri delle funzioni) Pattern matching
Spread { ...user, admin: true } Sintassi di aggiornamento delle struct: User { admin: true, ..user } Struct
switch, o catene di if su typeof / kind match (il compilatore controlla che tu abbia coperto ogni caso) match
=== ==, che confronta i valori (tramite il trait PartialEq). Mai nessuna conversione implicita di tipo Trait comuni
for...of for item in &items for
map, filter, reduce, find, some, every Metodi degli iteratori: map, filter, fold, find, any, all, poi collect Iteratori
Generatori (function*) e Symbol.iterator Implementa il trait Iterator Iteratori personalizzati
Promise<T> Un Future con Output = T. A differenza di una Promise, non fa niente finché non viene atteso con .await Async/await
async / await async fn e .await, più un runtime come tokio per eseguirli Async/await, Async in profondità
Promise.all tokio::join!, o join_all del crate futures Async in profondità
setTimeout tokio::time::sleep nel codice async, std::thread::sleep altrimenti Async/await
Web Worker, worker_threads Thread veri: std::thread::spawn, o rayon per i cicli paralleli Concorrenza
Moduli ES: import, export Moduli: mod, use e pub Moduli
JSON.parse / JSON.stringify serde_json con #[derive(Serialize, Deserialize)] JSON con Serde
console.log println!, e dbg! per il debug veloce. Usa {:?} per i tipi che fanno derive di Debug Output
readonly, as const, Object.freeze Non servono: tutto è di sola lettura, a meno che tu non scriva mut Variabili
Decoratori Attributi come #[derive(Debug)] e macro Macro
Number("abc") dà NaN "abc".parse::<i32>() restituisce un Err che devi gestire Conversione di tipo, Conversioni
Garbage collector Ownership e prestito, controllati in compilazione Ownership, Prestito
JavaScript Rust Per saperne di più
Array Vec<T> (tutti gli elementi hanno lo stesso tipo) Vettori
Array di lunghezza fissa, o una tupla TS [string, number] [T; N] per una lunghezza fissa, una tupla (String, i32) per tipi misti Array, Tuple
Map, o un oggetto usato come dizionario (Record<string, T>) HashMap<K, V> HashMap
Una mappa con le chiavi ordinate BTreeMap<K, V> HashMap
Set HashSet<T> (o BTreeSet<T> per l’ordine ordinato) HashSet
Array usato come coda (push + shift) VecDeque<T> (push_back / pop_front, entrambi veloci) VecDeque e BinaryHeap
Array usato come pila (push + pop) Vec<T> con push / pop Vettori
Uint8Array, Buffer Vec<u8>, o una slice di byte &[u8] Slice
WeakMap, WeakRef Weak<T> (da Rc o Arc) Weak e Cow

Una cosa a cui fare attenzione: in JavaScript Map, Set e gli oggetti semplici ricordano l’ordine in cui hai inserito gli elementi. HashMap e HashSet di Rust no. Se ti serve l’ordine ordinato, usa BTreeMap; se ti serve l’ordine di inserimento, il crate indexmap ti dà esattamente quello.

Con Cargo ti sentirai a casa. È il tuo gestore di pacchetti, strumento di build, runner dei test e degli script tutto in uno, e ogni progetto Rust lo usa allo stesso modo.

JavaScript Rust Note
node + npm (o pnpm, yarn) cargo Compila, esegue, testa, aggiunge pacchetti, pubblica. Vedi Cargo e crate
package.json Cargo.toml
package-lock.json Cargo.lock Viene creato per te
node_modules/ Nessuna cartella per progetto. I download finiscono una sola volta nella cache in ~/.cargo, il risultato della build va in target/
npmjs.com crates.io La documentazione delle API di ogni crate pubblicato è su docs.rs (in inglese)
npm install zod cargo add serde
npm init cargo new my_app (cargo new --lib per una libreria)
npm run dev / build / test cargo run / cargo build / cargo test Aggiungi --release per una build ottimizzata; le build di debug sono molto più lente
"scripts" in package.json Nessuna sezione di script. Usa gli alias di cargo in .cargo/config.toml, o un task runner come just
npx some-tool cargo install some-tool, poi eseguilo cargo binstall scarica binari già compilati invece di compilarli
nvm e .nvmrc rustup e un file rust-toolchain.toml Vedi Strumenti e configurazione
Workspace di npm / pnpm (monorepo) Un workspace Cargo Un Cargo.toml in cima elenca i crate membri
tsc rustc, chiamato per te da cargo
tsc --noEmit cargo check Controlla i tipi senza compilare il binario; molto più veloce
ESLint cargo clippy Centinaia di lint utili, ufficiali
Prettier cargo fmt Un solo stile standard per tutti
nodemon, tsx watch cargo watch o bacon Si installano con cargo install
Jest / Vitest / Mocha #[test] integrato e cargo test (cargo nextest è un runner più veloce molto usato) Vedi Test
Snapshot di Jest insta Vedi Test avanzati
jest.mock() mockall, o finti oggetti scritti a mano che implementano il tuo trait
fast-check proptest Test basati sulle proprietà
c8 / Istanbul (copertura) cargo llvm-cov
JSDoc / TypeDoc Commenti di documentazione /// in Markdown + cargo doc Gli esempi di codice nella documentazione vengono eseguiti come test. Vedi Documentazione
VS Code + il server di TypeScript VS Code + rust-analyzer, o JetBrains RustRover rust-analyzer offre completamento, vai-alla-definizione ed errori in linea
npm publish cargo publish

Ecco un piccolo Cargo.toml, così puoi confrontarlo con un package.json:

[package]
name = "orders-api"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

Una versione come "1" significa lo stesso di "^1" in package.json: qualsiasi release 1.x compatibile. Le feature sono parti opzionali di un crate che attivi tu; vedi Feature di Cargo e cfg.

Librerie e framework: «cosa usiamo al posto di…?»

Sezione intitolata “Librerie e framework: «cosa usiamo al posto di…?»”

È la prima domanda di ogni sviluppatore JavaScript. «Usiamo Express e Prisma. Qual è la versione Rust?» Ecco gli equivalenti più vicini. Come su npm, anche su crates.io ci sono molte scelte per quasi ogni lavoro; queste sono quelle popolari e ben mantenute.

JavaScript Rust Note
Express, Fastify, Koa axum (ne costruiamo una in Progetto: API web), actix-web, Rocket axum e actix-web sono molto usati in produzione
Middleware di Express Layer di tower e il crate tower-http (CORS, compressione, tracing, timeout) axum usa tower direttamente
fetch, axios reqwest Asincrono per default, con un client bloccante opzionale. Vedi Reti
ws, Socket.IO WebSocket in axum (la feature ws), tokio-tungstenite, o socketioxide (un server Socket.IO)
Apollo Server (GraphQL) async-graphql
gRPC (@grpc/grpc-js) tonic (con prost per protobuf)
Generatori Swagger / OpenAPI utoipa
jsonwebtoken jsonwebtoken Stesso nome, stessa idea
bcrypt argon2 o bcrypt
JavaScript Rust Note
Prisma, TypeORM SeaORM (asincrono, entità, migrazioni) o Diesel (query controllate sui tipi in compilazione) SeaORM è il più simile a Prisma e TypeORM
Knex, pg, postgres.js sqlx: scrivi SQL semplice, e la macro query! lo controlla sul tuo vero database in compilazione Asincrono, molto popolare, non è un ORM
better-sqlite3 rusqlite, o sqlx con SQLite
Mongoose, il driver MongoDB mongodb (il driver ufficiale) Niente schemi stile Mongoose; usi struct con serde
ioredis redis

Database in Rust mostra sqlx, Diesel e SeaORM con codice funzionante.

JavaScript Rust Note
JSON.parse / JSON.stringify serde + serde_json Lo standard; gestisce anche TOML, YAML e altro. Vedi JSON con Serde
Zod, Yup I tipi della tua struct + serde fanno quasi tutto il lavoro; validator o garde per regole come «almeno 8 caratteri» Se il JSON non corrisponde alla struct, il parsing fallisce con un errore
Lodash Quasi sempre gli iteratori della libreria standard; itertools per gli extra come unique, sorted_by e chunk_by
date-fns, Day.js, Luxon chrono o jiff Vedi Date e orari
dotenv dotenvy Vedi Ambiente e processi
Commander, yargs clap Vedi Progetto: tool CLI con clap
chalk colored o owo-colors
winston, pino tracing (+ tracing-subscriber), o log + env_logger Vedi Logging
uuid uuid
EventEmitter Canali (std::sync::mpsc, o tokio::sync::broadcast per molti ascoltatori) Vedi Canali
BullMQ (code di job) tokio::spawn per task nel processo; apalis per code di job persistenti
Espressioni regolari regex Niente lookahead né backreference, per scelta. Vedi Espressioni regolari
JavaScript Rust Note
Accelerare codice pesante nel browser Compila Rust in WebAssembly con wasm-bindgen e wasm-pack Vedi Rust e WebAssembly e la sezione più sotto
React, Vue, Svelte Leptos, Yew, Dioxus (tutti compilano in WebAssembly) Tutti prima della versione 1.0; le API cambiano ancora
Electron Tauri: il tuo frontend web + un backend Rust, con app molto più piccole Tauri 2 è stabile. Vedi App con interfaccia grafica
Addon nativi per Node.js (C++ con node-gyp) napi + napi-derive (napi-rs), o neon Pubblichi su npm binari già compilati; niente C++
Giochi con Phaser, Three.js Bevy, macroquad Bevy è prima della 1.0 e rompe le sue API tra una release e l’altra. Vedi Sviluppo di giochi

Fianco a fianco: un’interfaccia diventa una struct

Sezione intitolata “Fianco a fianco: un’interfaccia diventa una struct”

Ecco un oggetto TypeScript con un’interfaccia, una funzione che fa da metodo e una copia con lo spread:

interface User {
name: string;
age: number;
admin: boolean;
}
const greeting = (u: User) => `Hi, I'm ${u.name} (${u.age})`;
const ada: User = { name: "Ada", age: 36, admin: false };
const promoted = { ...ada, admin: true };
console.log(greeting(ada));
console.log(promoted);

In Rust la forma diventa una struct, e la funzione diventa un metodo in un blocco impl:

#[derive(Debug, Clone)]
struct User {
name: String,
age: u32,
admin: bool,
}
impl User {
// `&self` significa «questo metodo legge l'utente, non lo modifica».
fn greeting(&self) -> String {
format!("Ciao, sono {} ({})", self.name, self.age)
}
}
fn main() {
let ada = User { name: "Ada".to_string(), age: 36, admin: false };
// Come { ...ada, admin: true } in JavaScript.
let promoted = User { admin: true, ..ada.clone() };
println!("{}", ada.greeting());
println!("{:?}", promoted);
}

Output:

Ciao, sono Ada (36)
User { name: "Ada", age: 36, admin: true }
▶ Provalo tu

Alcune cose sono diverse. Ogni campo ha un tipo preciso: age è un u32 (un numero intero che non può essere negativo), non un number generico. #[derive(Debug)] è ciò che ti permette di stampare l’intera struct con {:?}, come console.log(obj). E perché .clone()? Lo spread ..ada sposterebbe la stringa name fuori da ada, e poi non potremmo più usare ada. Clonare prima lascia ada intatto. È l’ownership al lavoro, e la vedrai spesso.

Fianco a fianco: un’unione discriminata diventa un enum

Sezione intitolata “Fianco a fianco: un’unione discriminata diventa un enum”

Gli sviluppatori TypeScript adorano le unioni discriminate: un campo kind ti dice quale forma hai, e lo switch restringe il tipo:

type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle":
return Math.PI * s.radius ** 2;
case "rect":
return s.width * s.height;
}
}
const shapes: Shape[] = [
{ kind: "circle", radius: 1 },
{ kind: "rect", width: 2, height: 3 },
];
for (const s of shapes) console.log(area(s).toFixed(2));

In Rust questo è integrato nel linguaggio, ed è una delle sue caratteristiche migliori. Ogni variante di un enum porta i propri dati, e match li smonta:

enum Shape {
Circle { radius: f64 },
Rect { width: f64, height: f64 },
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rect { width, height } => width * height,
}
}
fn main() {
let shapes = vec![
Shape::Circle { radius: 1.0 },
Shape::Rect { width: 2.0, height: 3.0 },
];
for shape in &shapes {
println!("{:.2}", area(shape));
}
}

Output:

3.14
6.00
▶ Provalo tu

Non serve nessuna stringa kind: la variante stessa fa da etichetta. E match deve coprire ogni variante. Se più avanti aggiungi Triangle, ogni match che se ne dimentica diventa un errore di compilazione: è come il trucco dell’esaustività con never di TypeScript, ma sempre attivo. Trovi di più in Pattern matching.

Fianco a fianco: undefined e null diventano Option

Sezione intitolata “Fianco a fianco: undefined e null diventano Option”

In TypeScript i valori opzionali compaiono come undefined, null o una proprietà opzionale, e li gestisci con ?. e ??:

interface User {
name: string;
email?: string;
}
function findUser(id: number): User | undefined {
if (id === 1) return { name: "Ada", email: "[email protected]" };
if (id === 2) return { name: "Grace" };
return undefined;
}
for (const id of [1, 2, 3]) {
console.log(`${id}: ${findUser(id)?.email?.toUpperCase() ?? "no email"}`);
}

In Rust un valore che potrebbe mancare ha il tipo Option<T>. È o Some(valore) o None, e non puoi usarlo come un valore normale finché non hai gestito il caso None:

struct User {
name: String,
email: Option<String>,
}
fn find_user(id: u32) -> Option<User> {
match id {
1 => Some(User { name: "Ada".to_string(), email: Some("[email protected]".to_string()) }),
2 => Some(User { name: "Grace".to_string(), email: None }),
_ => None,
}
}
// Dentro una funzione che restituisce Option, `?` funziona come `?.`:
// se il valore è None, fermati qui e restituisci None.
fn email_domain(id: u32) -> Option<String> {
let email = find_user(id)?.email?;
let at = email.find('@')?;
Some(email[at + 1..].to_string())
}
fn main() {
for id in [1, 2, 3] {
// Come findUser(id)?.email?.toUpperCase() ?? "no email"
let email = find_user(id)
.and_then(|user| user.email)
.map(|email| email.to_uppercase())
.unwrap_or("nessuna email".to_string());
println!("{id}: {email}");
}
println!("{:?}", email_domain(1));
println!("{:?}", email_domain(2));
if let Some(user) = find_user(2) {
println!("utente trovato: {}", user.name);
}
}

Output:

2: nessuna email
3: nessuna email
Some("example.com")
None
utente trovato: Grace
▶ Provalo tu

In Rust non esiste «cannot read properties of undefined», perché non c’è nessun undefined da cui leggere. Il compilatore semplicemente non ti lascia chiamare .to_uppercase() su un Option<String>. E non devi ricordarti la differenza tra null e undefined: c’è solo None. Impara i metodi di supporto nella pagina Option, e le forme brevi in if let.

In JavaScript una funzione lancia un’eccezione, e tu catturi l’errore da qualche parte più in alto nello stack delle chiamate. Niente nella firma della funzione ti dice che potrebbe lanciarla:

function parseAge(input) {
const age = Number(input);
if (!Number.isInteger(age)) throw new Error(`'${input}' is not a whole number`);
if (age > 150) throw new Error(`${age} is too old`);
return age;
}
function totalAge(a, b) {
return parseAge(a) + parseAge(b);
}
for (const [a, b] of [["36", "41"], ["36", "abc"], ["36", "200"]]) {
try {
console.log(`Total: ${totalAge(a, b)}`);
} catch (e) {
console.log(`Error: ${e.message}`);
}
}

In Rust una funzione che può fallire restituisce un Result: o Ok(valore) o Err(errore). L’operatore ? significa «se questa cosa è fallita, restituisci subito l’errore a chi mi ha chiamato». Fa lo stesso lavoro di un’eccezione che risale, ma lo vedi nel codice:

fn parse_age(input: &str) -> Result<u32, String> {
let age: u32 = input
.parse()
.map_err(|_| format!("'{input}' non è un numero intero"))?;
if age > 150 {
return Err(format!("{age} è un'età troppo alta"));
}
Ok(age)
}
fn total_age(a: &str, b: &str) -> Result<u32, String> {
// Ogni `?` passa l'errore a chi ci ha chiamato, come farebbe un'eccezione.
let first = parse_age(a)?;
let second = parse_age(b)?;
Ok(first + second)
}
fn main() {
for (a, b) in [("36", "41"), ("36", "abc"), ("36", "200")] {
match total_age(a, b) {
Ok(total) => println!("Totale: {total}"),
Err(e) => println!("Errore: {e}"),
}
}
}

Output:

Totale: 77
Errore: 'abc' non è un numero intero
Errore: 200 è un'età troppo alta
▶ Provalo tu

La firma -> Result<u32, String> dice a chiunque chiami «questa cosa può fallire», e il compilatore non gli permette di ignorarlo. Lo stesso vale per il codice async: dove in JavaScript avresti una Promise rifiutata, una funzione async di Rust restituisce un Result, e tu scrivi .await?. Per i tuoi tipi di errore, vedi Errori personalizzati. Nelle applicazioni, il crate anyhow ti dà qualcosa di simile a «cattura qualsiasi errore» (vedi anyhow e thiserror).

Fianco a fianco: i metodi degli array diventano iteratori

Sezione intitolata “Fianco a fianco: i metodi degli array diventano iteratori”

I metodi degli array che usi tutti i giorni hanno equivalenti diretti in Rust:

const orders = [
{ customer: "Ada", total: 120, paid: true },
{ customer: "Grace", total: 80, paid: false },
{ customer: "Linus", total: 200, paid: true },
];
const paidNames = orders.filter((o) => o.paid).map((o) => o.customer);
const revenue = orders.filter((o) => o.paid).reduce((sum, o) => sum + o.total, 0);
const firstBig = orders.find((o) => o.total > 150);
console.log(paidNames.join(", "));
console.log(`Revenue: ${revenue}`);
console.log(`First big order: ${firstBig?.customer ?? "none"}`);
console.log(`Anything unpaid? ${orders.some((o) => !o.paid)}`);

La versione Rust usa gli iteratori e le closure. La sintassi delle closure è |o| o.paid invece di (o) => o.paid:

struct Order {
customer: String,
total: u32,
paid: bool,
}
fn main() {
let orders = vec![
Order { customer: "Ada".to_string(), total: 120, paid: true },
Order { customer: "Grace".to_string(), total: 80, paid: false },
Order { customer: "Linus".to_string(), total: 200, paid: true },
];
// filter + map, poi collect() trasforma il risultato in un Vec.
let paid_names: Vec<&str> = orders
.iter()
.filter(|o| o.paid)
.map(|o| o.customer.as_str())
.collect();
// reduce con un valore iniziale si chiama fold.
let revenue = orders
.iter()
.filter(|o| o.paid)
.fold(0, |sum, o| sum + o.total);
// find restituisce un Option, perché potrebbe non trovare niente.
let first_big = orders.iter().find(|o| o.total > 150);
println!("{}", paid_names.join(", "));
println!("Incasso: {revenue}");
match first_big {
Some(o) => println!("Primo ordine grande: {}", o.customer),
None => println!("Primo ordine grande: nessuno"),
}
// some() si chiama any().
println!("Ordini non pagati? {}", orders.iter().any(|o| !o.paid));
}

Output:

Ada, Linus
Incasso: 320
Primo ordine grande: Linus
Ordini non pagati? true
▶ Provalo tu

C’è una grande differenza rispetto a JavaScript. In JS ogni metodo di un array costruisce subito un array nuovo. Gli iteratori di Rust sono pigri (lazy): filter e map da soli non fanno niente, e il lavoro avviene in un solo passaggio quando chiami qualcosa come collect(), sum(), fold() o un ciclo for. In mezzo non vengono creati array temporanei. Per la sola somma potresti anche scrivere .map(|o| o.total).sum::<u32>(). L’ordinamento sta su Vec (sort, sort_by), non sugli iteratori; vedi Ordinamento e ricerca.

Fianco a fianco: le Promise diventano future + tokio

Sezione intitolata “Fianco a fianco: le Promise diventano future + tokio”

Ecco un po’ di JavaScript async. Guarda quando viene stampato "fetching 1":

async function fetchUser(id) {
console.log(`fetching ${id}`);
await new Promise((resolve) => setTimeout(resolve, 100));
return `user ${id}`;
}
const promise = fetchUser(1); // stampa subito "fetching 1"
console.log("promise created");
console.log(await promise);
const [a, b] = await Promise.all([fetchUser(2), fetchUser(3)]);
console.log(`${a}, ${b}`);

In JavaScript, chiamare una funzione async la fa partire subito. Rust è diverso: chiamare una async fn crea solo un future, una descrizione del lavoro da fare. Non parte niente finché non lo attendi con .await. E Rust non ha un event loop integrato, quindi aggiungi un runtime: tokio (in inglese) è il più popolare.

use std::time::Duration;
async fn fetch_user(id: u32) -> String {
println!("carico {id}");
tokio::time::sleep(Duration::from_millis(100)).await;
format!("utente {id}")
}
#[tokio::main]
async fn main() {
// Creare un future NON lo fa partire. Per ora non viene stampato niente.
let future = fetch_user(1);
println!("future creato");
println!("{}", future.await);
// Come Promise.all: esegui entrambi insieme e aspettali tutti e due.
let (a, b) = tokio::join!(fetch_user(2), fetch_user(3));
println!("{a}, {b}");
}

Output:

future creato
carico 1
utente 1
carico 2
carico 3
utente 2, utente 3
▶ Provalo tu

Vedi l’ordine? In JavaScript, «fetching 1» arriverebbe prima di «promise created». In Rust, «carico 1» arriva dopo «future creato», perché il future parte solo con .await. #[tokio::main] prepara il runtime (il tuo event loop) ed esegue main al suo interno. tokio::join! aspetta i due future insieme, come Promise.all; tokio::spawn avvia un task in background, che è la cosa più vicina a chiamare una funzione async senza attenderla. Le lezioni Async/await e Async in profondità spiegano i dettagli.

Rust nel tuo progetto JavaScript: WebAssembly e addon per Node

Sezione intitolata “Rust nel tuo progetto JavaScript: WebAssembly e addon per Node”

Non devi riscrivere niente per iniziare a usare Rust. Ci sono due modi comuni per inserirlo in un progetto JavaScript:

WebAssembly nel browser. Segna le funzioni Rust con #[wasm_bindgen], compila con wasm-pack e importa il risultato come qualsiasi modulo ES. wasm-pack scrive perfino un file .d.ts, così TypeScript conosce i tipi delle tue funzioni:

use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn slugify(title: &str) -> String {
title
.to_lowercase()
.split_whitespace()
.collect::<Vec<_>>()
.join("-")
}
import init, { slugify } from "./pkg/my_lib.js";
await init(); // scarica e avvia il file .wasm
console.log(slugify("Hello Rust World")); // "hello-rust-world"

La lezione Rust e WebAssembly ti guida in tutta la configurazione, e spiega quali valori attraversano il confine senza costi. Un consiglio: Wasm conviene per i calcoli pesanti (elaborazione di immagini, parsing, simulazioni), non per piccole funzioni chiamate migliaia di volte, perché ogni chiamata tra JS e Wasm ha un piccolo costo.

Addon nativi per Node.js. Con napi-rs (in inglese; i crate napi e napi-derive) segni una funzione Rust con #[napi], e diventa una funzione che puoi caricare con require o import da Node. Molti pacchetti npm popolari distribuiscono così binari Rust già compilati, e gli utenti non hanno mai bisogno di un compilatore Rust.

Le cose che ti faranno inciampare se vieni da JavaScript

Sezione intitolata “Le cose che ti faranno inciampare se vieni da JavaScript”

Chiunque arrivi da JavaScript prende le stesse buche. Conoscerle in anticipo aiuta.

Passare un valore a una funzione lo sposta. In JavaScript, passare un array a una funzione passa un riferimento, e dopo puoi continuare a usare l’array. In Rust, passare per valore un Vec o una String sposta l’ownership dentro la funzione, e la variabile originale non si può più usare:

fn print_all(names: Vec<String>) {
for name in names {
println!("{name}");
}
}
fn main() {
let names = vec![String::from("Ada"), String::from("Grace")];
print_all(names);
println!("{} names", names.len());
}
error[E0382]: borrow of moved value: `names`
--> src/main.rs:10:26
|
8 | let names = vec![String::from("Ada"), String::from("Grace")];
| ----- move occurs because `names` has type `Vec<String>`, which does not implement the `Copy` trait
9 | print_all(names);
| ----- value moved here
10 | println!("{} names", names.len());
| ^^^^^ value borrowed here after move

Il compilatore dice che names è stato spostato nella chiamata a print_all e poi usato di nuovo. La soluzione abituale è prendere in prestito: fai in modo che la funzione accetti names: &[String] e chiamala con print_all(&names). Leggi prima Ownership e Prestito: spiegano questa regola, e la incontrerai ogni giorno.

I numeri hanno un tipo, e non si mescolano. JavaScript ha un solo number. In Rust 3 è un intero e 2.5 è un float, e non puoi combinarli senza una conversione:

fn main() {
let items = 3;
let price = 2.5;
let total = items * price;
println!("{total}");
}
error[E0277]: cannot multiply `{integer}` by `{float}`
--> src/main.rs:4:23
|
4 | let total = items * price;
| ^ no implementation for `{integer} * {float}`

Il messaggio dice che non si può moltiplicare un intero per un float. Scrivi items as f64 * price, o rendi items un float fin dall’inizio (3.0). Nota anche che la divisione intera scarta il resto: 7 / 2 vale 3, non 3.5. Vedi Tipi di dati e Conversione di tipo.

Eccone altre che incontrerai:

  • I future sono pigri e hanno bisogno di un runtime. Come hai visto sopra, non succede niente fino a .await, e ti serve tokio (o un altro runtime) per eseguire codice async. Se dimentichi .await ottieni un avviso del compilatore, non un task in esecuzione.
  • Le stringhe non si possono indicizzare. name[0] non compila, perché una String di Rust è UTF-8 e un carattere può occupare più byte. Usa name.chars().next() o .chars().nth(i). E impara presto la differenza tra String e &str: di solito le funzioni accettano &str e le struct contengono String. Vedi Stringhe.
  • Niente tipizzazione strutturale e niente any. In TypeScript qualsiasi oggetto con i campi giusti va bene per un’interfaccia. In Rust una funzione che accetta un User accetta solo un User. Per il comportamento condiviso usa i trait. Per un JSON di cui non conosci la forma usa serde_json::Value.
  • I grafi di oggetti condivisi e modificabili sono difficili. In JavaScript è normale passare in giro un oggetto e lasciare che tutti lo modifichino, o avere oggetti che puntano al loro genitore. Il borrow checker permette molti lettori oppure un solo scrittore alla volta, mai entrambi. Schemi del genere richiedono smart pointer (Rc<RefCell<T>>, Arc<Mutex<T>>) o, meglio, un design diverso. Mettiti in conto qualche settimana frustrante. Poi diventa molto più facile.
  • HashMap non ha ordine. In JavaScript Map e oggetti mantengono l’ordine di inserimento, e spesso il codice ci conta senza accorgersene. Una HashMap di Rust no. Ordina prima di stampare, o usa BTreeMap o il crate indexmap.
  • Niente eccezioni. Gli errori sono valori, e scriverai ? molto spesso. Chiamare .unwrap() va bene mentre impari, ma è la versione Rust del ! di TypeScript: «fidati, c’è». Se sbagli, il programma va in panic.
  • Tempi di compilazione. Non c’è il ricaricamento istantaneo di node app.js. Una prima build con molti crate richiede un po’ di tempo; le build successive sono incrementali e più veloci. Usa cargo check mentre scrivi, perché salta la generazione del codice.

Non devi leggere questo sito in ordine. Da sviluppatore JavaScript o TypeScript, questo percorso salta quello che sai già e si sofferma dove Rust è diverso:

  1. Primi passi e Strumenti e configurazione: installa Rust, configura VS Code con rust-analyzer, esegui cargo new.
  2. Variabili e Tipi di dati: leggile con attenzione. I tipi numerici sono una novità per te.
  3. Ownership, poi Prestito, poi Slice: le pagine più importanti per te. Prenditi il tuo tempo.
  4. Stringhe: String contro &str.
  5. Struct ed Enum, poi match e Pattern matching: le tue interfacce e unioni discriminate.
  6. Option e Result, poi Errori personalizzati.
  7. Closure e Iteratori: i tuoi metodi degli array.
  8. Vettori, HashMap e HashSet.
  9. Trait, Generici e Trait comuni.
  10. Moduli, Cargo e crate e Test.
  11. Lifetime e Smart pointer.
  12. Async/await e Async in profondità.
  13. JSON con Serde, poi costruisci il progetto API web, o prova WebAssembly.

Entrambi gli esercizi ti danno un frammento di JavaScript o TypeScript. Portalo in Rust.

Questo codice JavaScript conta quante volte compare ogni parola, poi stampa le parole dalla più frequente alla meno frequente (a parità, in ordine alfabetico). Portalo in Rust. Suggerimento: usa una HashMap<&str, u32> e *counts.entry(word).or_insert(0) += 1. Per ordinare, raccogli la mappa in un Vec di coppie (parola, conteggio) e usa sort_by.

const text = "the cat and the hat and the bat";
const counts = new Map();
for (const word of text.split(" ")) {
counts.set(word, (counts.get(word) ?? 0) + 1);
}
const sorted = [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
for (const [word, n] of sorted) console.log(`${word}: ${n}`);
Mostra la soluzione
use std::collections::HashMap;
fn main() {
let text = "the cat and the hat and the bat";
let mut counts: HashMap<&str, u32> = HashMap::new();
for word in text.split(' ') {
*counts.entry(word).or_insert(0) += 1;
}
// Come [...counts]: trasforma la mappa in una lista di coppie (parola, conteggio).
let mut sorted: Vec<(&str, u32)> = counts.into_iter().collect();
// Prima le più frequenti; a parità, in ordine alfabetico.
sorted.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (word, n) in sorted {
println!("{word}: {n}");
}
}

Output:

the: 3
and: 2
bat: 1
cat: 1
hat: 1
▶ Provalo tu

entry(word).or_insert(0) fa in un solo passaggio la parte counts.get(word) ?? 0 e il set: ti dà un riferimento mutabile al conteggio, creandolo con 0 se manca. Qui l’ordinamento è essenziale, non solo un tocco in più: senza, una HashMap stampa in un ordine diverso da un’esecuzione all’altra. Il testo resta in inglese perché il risultato dipende dall’ordine alfabetico delle parole.

Questa funzione TypeScript accetta un oggetto di opzioni in cui ogni campo è opzionale, e riempie i valori predefiniti con ??. Portala in Rust. Suggerimento: crea una struct con campi Option e aggiungi #[derive(Default)]. Poi ServerOptions { port: Some(3000), ..Default::default() } funziona come passare { port: 3000 }. Per un bool, il valore predefinito è false.

interface ServerOptions {
host?: string;
port?: number;
verbose?: boolean;
}
function describe(opts: ServerOptions): string {
const host = opts.host ?? "localhost";
const port = opts.port ?? 8080;
const mode = opts.verbose ? "verbose" : "quiet";
return `${host}:${port} (${mode})`;
}
console.log(describe({}));
console.log(describe({ port: 3000 }));
console.log(describe({ host: "example.com", verbose: true }));
Mostra la soluzione
#[derive(Default)]
struct ServerOptions {
host: Option<String>,
port: Option<u16>,
verbose: bool,
}
fn describe(opts: &ServerOptions) -> String {
let host = opts.host.as_deref().unwrap_or("localhost");
let port = opts.port.unwrap_or(8080);
let mode = if opts.verbose { "verbose" } else { "quiet" };
format!("{host}:{port} ({mode})")
}
fn main() {
println!("{}", describe(&ServerOptions::default()));
println!("{}", describe(&ServerOptions { port: Some(3000), ..Default::default() }));
println!(
"{}",
describe(&ServerOptions {
host: Some("example.com".to_string()),
verbose: true,
..Default::default()
})
);
}

Output:

localhost:8080 (quiet)
localhost:3000 (quiet)
example.com:8080 (verbose)
▶ Provalo tu

unwrap_or è il tuo ??. as_deref() trasforma un Option<String> in un Option<&str>, così può ripiegare sul testo semplice "localhost" senza copiare niente. E ..Default::default() è il modo di Rust per dire «ogni campo che non ho nominato prende il suo valore predefinito», che è esattamente quello che fa in JavaScript omettere una proprietà. Una porta di tipo u16 significa anche che un valore come 70000 non può nemmeno essere salvato per errore.

  • Molto di JavaScript e TypeScript si porta dietro: closure, async/await, destrutturazione e metodi in stile array come map, filter e find hanno tutti equivalenti vicini in Rust.
  • I grandi cambiamenti sono l’ownership al posto del garbage collector, Option al posto di undefined e null, e Result + ? al posto di throw e try/catch.
  • Le interfacce di TypeScript diventano struct (per i dati) o trait (per il comportamento), e le unioni discriminate diventano enum con match.
  • I future di Rust sono pigri e hanno bisogno di un runtime come tokio; tokio::join! fa la parte di Promise.all.
  • Cargo sostituisce npm, package.json e i tuoi script; clippy e rustfmt sostituiscono ESLint e Prettier; i test sono integrati.
  • Al posto di Express guarda axum o actix-web, al posto di fetch guarda reqwest, al posto di Prisma guarda SeaORM o Diesel, e al posto di Electron guarda Tauri. Puoi anche portare Rust in un progetto JS tramite WebAssembly o napi-rs.

Verifica veloce

1. Cosa usa Rust al posto di undefined e null?

2. In JavaScript, chiamare una funzione async la fa partire subito. Cosa succede in Rust quando chiami una async fn senza .await?

3. Il tuo codice TypeScript usa un'unione discriminata con un campo kind. Qual è la caratteristica di Rust più vicina?

4. Il backend del tuo team usa Express. Quale crate Rust è il più simile?

Punteggio: 0 / 4

  • The Rust Programming Language (in inglese): il libro ufficiale. I capitoli 4 (ownership), 6 (enum e pattern matching) e 9 (errori) sono i più importanti per chi viene da JavaScript.
  • Tokio tutorial (in inglese): la guida ufficiale a Rust asincrono con tokio, dal primo task fino a canali e I/O.
  • The wasm-bindgen Guide (in inglese): come Rust e JavaScript si parlano in WebAssembly, con molti esempi.
  • Compiling from Rust to WebAssembly (MDN) (in inglese): un tutorial passo passo di MDN Web Docs, compresa la pubblicazione su npm.
  • napi-rs (in inglese): la documentazione per scrivere addon di Node.js in Rust.