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 |
Collezioni
Sezione intitolata “Collezioni”| 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.
Strumenti: da npm a cargo
Sezione intitolata “Strumenti: da npm a cargo”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.
Web e rete
Sezione intitolata “Web e rete”| 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 |
Accesso ai dati
Sezione intitolata “Accesso ai dati”| 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.
I mattoni di tutti i giorni
Sezione intitolata “I mattoni di tutti i giorni”| 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 |
Frontend, desktop e Node.js
Sezione intitolata “Frontend, desktop e Node.js”| 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 }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.146.00Non 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 === 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 { 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 email3: nessuna emailSome("example.com")Noneutente trovato: GraceIn 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.
Fianco a fianco: try/catch diventa Result e ?
Sezione intitolata “Fianco a fianco: try/catch diventa Result e ?”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: 77Errore: 'abc' non è un numero interoErrore: 200 è un'età troppo altaLa 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, LinusIncasso: 320Primo ordine grande: LinusOrdini non pagati? trueC’è 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 creatocarico 1utente 1carico 2carico 3utente 2, utente 3Vedi 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 .wasmconsole.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 here10 | println!("{} names", names.len()); | ^^^^^ value borrowed here after moveIl 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.awaitottieni un avviso del compilatore, non un task in esecuzione. - Le stringhe non si possono indicizzare.
name[0]non compila, perché unaStringdi Rust è UTF-8 e un carattere può occupare più byte. Usaname.chars().next()o.chars().nth(i). E impara presto la differenza traStringe&str: di solito le funzioni accettano&stre le struct contengonoString. 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 unUseraccetta solo unUser. Per il comportamento condiviso usa i trait. Per un JSON di cui non conosci la forma usaserde_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. HashMapnon ha ordine. In JavaScriptMape oggetti mantengono l’ordine di inserimento, e spesso il codice ci conta senza accorgersene. UnaHashMapdi Rust no. Ordina prima di stampare, o usaBTreeMapo il crateindexmap.- 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. Usacargo checkmentre scrivi, perché salta la generazione del codice.
Un percorso di studio per sviluppatori JavaScript
Sezione intitolata “Un percorso di studio per sviluppatori JavaScript”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:
- Primi passi e Strumenti e configurazione: installa Rust, configura VS Code con rust-analyzer, esegui
cargo new. - Variabili e Tipi di dati: leggile con attenzione. I tipi numerici sono una novità per te.
- Ownership, poi Prestito, poi Slice: le pagine più importanti per te. Prenditi il tuo tempo.
- Stringhe:
Stringcontro&str. - Struct ed Enum, poi match e Pattern matching: le tue interfacce e unioni discriminate.
- Option e Result, poi Errori personalizzati.
- Closure e Iteratori: i tuoi metodi degli array.
- Vettori, HashMap e HashSet.
- Trait, Generici e Trait comuni.
- Moduli, Cargo e crate e Test.
- Lifetime e Smart pointer.
- Async/await e Async in profondità.
- JSON con Serde, poi costruisci il progetto API web, o prova WebAssembly.
Pratica
Sezione intitolata “Pratica”Entrambi gli esercizi ti danno un frammento di JavaScript o TypeScript. Portalo in Rust.
1. Conta le parole con una Map
Sezione intitolata “1. Conta le parole con una Map”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: 3and: 2bat: 1cat: 1hat: 1entry(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.
2. Porta delle opzioni con valori predefiniti
Sezione intitolata “2. Porta delle opzioni con valori predefiniti”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)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.
Riepilogo
Sezione intitolata “Riepilogo”- Molto di JavaScript e TypeScript si porta dietro: closure, async/await, destrutturazione e metodi in stile array come
map,filterefindhanno tutti equivalenti vicini in Rust. - I grandi cambiamenti sono l’ownership al posto del garbage collector,
Optional posto diundefinedenull, eResult+?al posto dithrowetry/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 diPromise.all. - Cargo sostituisce npm,
package.jsone 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
fetchguarda 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
Sezione intitolata “Verifica veloce”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
Per approfondire
Sezione intitolata “Per approfondire”- 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.