Rust pour les développeurs JavaScript et TypeScript
Si vous écrivez du JavaScript ou du TypeScript, vous connaissez déjà plus de Rust que vous ne le pensez. Rust a des closures qui ressemblent beaucoup aux fonctions fléchées, async/await, des méthodes d’itérateurs aux noms familiers comme map, filter et find, la déstructuration, et un seul outil en ligne de commande (Cargo) qui fait ce que fait npm. Si vous utilisez TypeScript, vous avez aussi l’habitude d’un compilateur qui attrape les erreurs avant que votre code ne s’exécute. Le compilateur de Rust fait la même chose, en plus strict.
Alors, qu’est-ce qui est vraiment nouveau ? Trois choses. Rust n’a pas de ramasse-miettes (garbage collector) : chaque valeur a un seul propriétaire, et le compilateur vérifie l’ownership (possession) et le borrowing (emprunt) avant que votre programme ne s’exécute. Rust n’a ni undefined ni null : une valeur qui peut manquer a le type Option. Et Rust n’a pas d’exceptions : une fonction qui peut échouer renvoie un Result. C’est sur ces trois idées que vous passerez le plus de temps d’apprentissage.
Pourquoi les développeurs JavaScript apprennent-ils Rust ? En général pour l’une de ces raisons : vous voulez un backend web ou un outil en ligne de commande rapide, livré sous forme d’un seul fichier ; vous voulez accélérer un point chaud dans le navigateur avec WebAssembly ; vous voulez écrire un module natif (addon) pour Node.js ; ou vous êtes curieux de savoir comment sont faits les outils que vous utilisez déjà. SWC, Biome, Oxc, Rolldown, Rspack, Turbopack, Lightning CSS et Deno sont tous écrits en Rust.
Cette page fait le lien entre ce que vous connaissez de JavaScript et TypeScript et Rust, pour que vous puissiez consacrer votre temps à ce qui est vraiment nouveau. Si vous cherchez plutôt une comparaison générale avec C++, Go et Python, voyez Rust vs C++, Go et Python.
Les concepts du langage : de JavaScript et TypeScript à Rust
Section intitulée « Les concepts du langage : de JavaScript et TypeScript à Rust »Utilisez ce tableau comme un guide de conversation. La dernière colonne renvoie à la leçon de ce site qui explique le côté Rust.
| JavaScript / TypeScript | Rust | Pour en savoir plus |
|---|---|---|
let et const |
let (non modifiable) et let mut (modifiable). Un objet const en JS peut encore être modifié de l’intérieur ; une valeur liée par let en Rust, non |
Variables |
number |
Vous choisissez une taille : i32, i64, u32, usize, f64, … Les entiers et les flottants sont des types différents |
Types de données |
bigint |
i128 / u128, ou la crate num-bigint pour une taille quelconque |
Types de données |
string |
String (possédée, qui peut grandir) et &str (une vue empruntée sur du texte) |
Chaînes |
boolean |
bool |
Booléens |
| Littéraux de gabarit (template literals) | format!("Hi {name}") et println!("Hi {name}") |
Afficher du texte |
any, unknown |
Pas de any. Utilisez des génériques, un enum, ou serde_json::Value pour du JSON de forme inconnue |
Génériques, JSON avec Serde |
interface ou type qui décrit la forme d’un objet |
struct |
Structs |
interface qui décrit des méthodes |
trait |
Traits |
| Typage structurel (même forme = même type) | Typage nominal : deux structs avec les mêmes champs restent des types différents | Structs |
| Types union et unions discriminées | enum dont chaque variante peut porter ses propres données |
Enums |
Types littéraux de chaîne comme "small" ou "large" |
Un enum avec des variantes simples : enum Size { Small, Large } |
Enums |
undefined, null, propriété optionnelle x?: T |
Option<T> : soit Some(value), soit None |
Option |
?. et ?? |
? (dans une fonction qui renvoie une Option), .map(...), .and_then(...), .unwrap_or(...) |
Option |
Assertion non nulle x! |
.unwrap() ou .expect("raison"), qui arrêtent le programme si la valeur est None |
Panic |
throw, try/catch, Promises rejetées |
Result<T, E> + l’opérateur ? |
Result |
class MyError extends Error |
Votre propre enum d’erreur (souvent avec thiserror) ; anyhow dans les applications |
Erreurs personnalisées, anyhow et thiserror |
class avec des champs et des méthodes |
struct pour les données + un bloc impl pour les méthodes |
Structs |
extends (héritage) |
Pas d’héritage. Utilisez la composition (une struct dans une struct) + des traits avec des méthodes par défaut | Traits, Patrons de conception |
class Dog implements Animal |
impl Animal for Dog |
Traits |
| Un tableau de classes différentes qui partagent une interface | Vec<Box<dyn Animal>> (un trait object) |
Trait objects |
Génériques comme T extends HasId |
Génériques avec des contraintes de trait : T: HasId |
Génériques |
| Fonctions fléchées et callbacks | Closures, décrites par les traits Fn, FnMut, FnOnce |
Closures, Fn, FnMut et FnOnce |
this |
Un paramètre explicite self, &self ou &mut self. Aucune surprise sur ce que désigne this |
Structs |
Déstructuration const { x, y } = point |
Motifs (patterns) : let Point { x, y } = point; (aussi dans match et dans les paramètres de fonction) |
Pattern matching |
Spread { ...user, admin: true } |
Syntaxe de mise à jour de struct : User { admin: true, ..user } |
Structs |
switch, ou chaînes de if sur typeof / kind |
match (le compilateur vérifie que vous avez couvert tous les cas) |
match |
=== |
==, qui compare des valeurs (via le trait PartialEq). Jamais de conversion de type implicite |
Traits courants |
for...of |
for item in &items |
Boucles for |
map, filter, reduce, find, some, every |
Méthodes d’itérateurs : map, filter, fold, find, any, all, puis collect |
Itérateurs |
Générateurs (function*) et Symbol.iterator |
Implémenter le trait Iterator |
Itérateurs personnalisés |
Promise<T> |
Un Future avec Output = T. Contrairement à une Promise, il ne fait rien tant qu’on ne l’attend pas |
Async/Await |
async / await |
async fn et .await, plus un runtime comme tokio pour les exécuter |
Async/Await, Async en profondeur |
Promise.all |
tokio::join!, ou join_all de la crate futures |
Async en profondeur |
setTimeout |
tokio::time::sleep dans du code async, std::thread::sleep sinon |
Async/Await |
Web Workers, worker_threads |
De vrais threads : std::thread::spawn, ou rayon pour des boucles parallèles |
Concurrence |
Modules ES : import, export |
Modules : mod, use et pub |
Modules |
JSON.parse / JSON.stringify |
serde_json avec #[derive(Serialize, Deserialize)] |
JSON avec Serde |
console.log |
println!, et dbg! pour un débogage rapide. Utilisez {:?} pour les types qui dérivent Debug |
Afficher du texte |
readonly, as const, Object.freeze |
Inutile : tout est en lecture seule tant que vous n’écrivez pas mut |
Variables |
| Décorateurs | Des attributs comme #[derive(Debug)] et des macros |
Macros |
Number("abc") donne NaN |
"abc".parse::<i32>() renvoie un Err que vous devez traiter |
Conversion de types, Conversions |
| Ramasse-miettes | Ownership et borrowing, vérifiés à la compilation | Ownership, Borrowing |
Les collections
Section intitulée « Les collections »| JavaScript | Rust | Pour en savoir plus |
|---|---|---|
Array |
Vec<T> (tous les éléments ont le même type) |
Vecteurs |
Tableau de longueur fixe, ou tuple TS [string, number] |
[T; N] pour une longueur fixe, un tuple (String, i32) pour des types mélangés |
Tableaux, Tuples |
Map, ou un objet utilisé comme dictionnaire (Record<string, T>) |
HashMap<K, V> |
HashMap |
| Une map avec des clés triées | BTreeMap<K, V> |
HashMap |
Set |
HashSet<T> (ou BTreeSet<T> pour un ordre trié) |
HashSet |
Tableau utilisé comme file (push + shift) |
VecDeque<T> (push_back / pop_front, tous deux rapides) |
VecDeque et BinaryHeap |
Tableau utilisé comme pile (push + pop) |
Vec<T> avec push / pop |
Vecteurs |
Uint8Array, Buffer |
Vec<u8>, ou une slice d’octets &[u8] |
Slices |
WeakMap, WeakRef |
Weak<T> (à partir d’un Rc ou d’un Arc) |
Weak et Cow |
Un point d’attention : en JavaScript, une Map, un Set et un objet ordinaire retiennent l’ordre d’insertion. Une HashMap et un HashSet Rust, non. S’il vous faut un ordre trié, utilisez BTreeMap ; s’il vous faut l’ordre d’insertion, la crate indexmap fait exactement ça.
L’outillage : de npm à cargo
Section intitulée « L’outillage : de npm à cargo »Avec Cargo, vous vous sentirez chez vous. C’est à la fois votre gestionnaire de paquets, votre outil de build, votre lanceur de tests et votre lanceur de scripts, et tous les projets Rust l’utilisent de la même façon.
| JavaScript | Rust | Remarques |
|---|---|---|
node + npm (ou pnpm, yarn) |
cargo |
Compile, exécute, teste, ajoute des paquets, publie. Voir Cargo et les crates |
package.json |
Cargo.toml |
|
package-lock.json |
Cargo.lock |
Créé pour vous |
node_modules/ |
Pas de dossier par projet. Les téléchargements sont mis en cache une seule fois dans ~/.cargo, et le résultat de la compilation va dans target/ |
|
| npmjs.com | crates.io | La documentation de l’API de chaque crate publiée est sur docs.rs |
npm install zod |
cargo add serde |
|
npm init |
cargo new my_app (cargo new --lib pour une bibliothèque) |
|
npm run dev / build / test |
cargo run / cargo build / cargo test |
Ajoutez --release pour un build optimisé ; les builds debug sont beaucoup plus lents |
"scripts" dans package.json |
Pas de section scripts. Utilisez des alias cargo dans .cargo/config.toml, ou un lanceur de tâches comme just |
|
npx some-tool |
cargo install some-tool, puis lancez-le |
cargo binstall télécharge des binaires précompilés au lieu de compiler |
nvm et .nvmrc |
rustup et un fichier rust-toolchain.toml |
Voir Les outils de Rust |
| Workspaces npm / pnpm (monorepo) | Un workspace Cargo | Un Cargo.toml à la racine liste les crates membres |
tsc |
rustc, appelé pour vous par cargo |
|
tsc --noEmit |
cargo check |
Vérifie les types sans compiler jusqu’au bout ; beaucoup plus rapide |
| ESLint | cargo clippy |
Des centaines de lints utiles, officiels |
| Prettier | cargo fmt |
Un seul style standard pour tout le monde |
nodemon, tsx watch |
cargo watch ou bacon |
Installés avec cargo install |
| Jest / Vitest / Mocha | #[test] et cargo test intégrés (cargo nextest est un lanceur plus rapide très populaire) |
Voir Les tests |
| Snapshots Jest | insta |
Voir Tests avancés |
jest.mock() |
mockall, ou de fausses implémentations écrites à la main qui implémentent votre trait |
|
| fast-check | proptest |
Tests basés sur les propriétés |
| c8 / Istanbul (couverture de code) | cargo llvm-cov |
|
| JSDoc / TypeDoc | Commentaires de doc /// en Markdown + cargo doc |
Les exemples de code dans la doc sont exécutés comme des tests. Voir Documentation |
| VS Code + le serveur TypeScript | VS Code + rust-analyzer, ou JetBrains RustRover | rust-analyzer offre la complétion, l’accès à la définition et les erreurs dans le code |
npm publish |
cargo publish |
Voici un petit Cargo.toml, pour que vous puissiez le comparer à 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"] }Une version comme "1" veut dire la même chose que "^1" dans package.json : n’importe quelle version 1.x compatible. Les features sont des parties optionnelles d’une crate que vous activez ; voir Features Cargo et cfg.
Bibliothèques et frameworks : « on utilise quoi à la place de… ? »
Section intitulée « Bibliothèques et frameworks : « on utilise quoi à la place de… ? » »C’est la première question que pose chaque développeur JavaScript. « On utilise Express et Prisma. C’est quoi, la version Rust ? » Voici les équivalents les plus proches. Comme sur npm, crates.io propose plusieurs choix pour la plupart des tâches ; voici les plus populaires et les mieux maintenus.
Web et réseau
Section intitulée « Web et réseau »| JavaScript | Rust | Remarques |
|---|---|---|
| Express, Fastify, Koa | axum (nous en construisons une dans Projet : API web avec Axum), actix-web, Rocket | axum et actix-web sont très utilisés en production |
| Middlewares Express | Les layers tower et la crate tower-http (CORS, compression, traçage, timeouts) |
axum utilise tower directement |
fetch, axios |
reqwest |
Async par défaut, avec un client bloquant en option. Voir Réseau en Rust |
ws, Socket.IO |
Les WebSockets dans axum (la feature ws), tokio-tungstenite, ou socketioxide (un serveur Socket.IO) |
|
| Apollo Server (GraphQL) | async-graphql |
|
gRPC (@grpc/grpc-js) |
tonic (avec prost pour protobuf) |
|
| Générateurs Swagger / OpenAPI | utoipa |
|
jsonwebtoken |
jsonwebtoken |
Même nom, même idée |
bcrypt |
argon2 ou bcrypt |
Accès aux données
Section intitulée « Accès aux données »| JavaScript | Rust | Remarques |
|---|---|---|
| Prisma, TypeORM | SeaORM (async, entités, migrations) ou Diesel (requêtes vérifiées par le système de types à la compilation) | SeaORM est le plus proche de Prisma et TypeORM |
Knex, pg, postgres.js |
sqlx : vous écrivez du SQL brut, et la macro query! le vérifie à la compilation contre votre vraie base de données |
Async, très populaire, pas un ORM |
better-sqlite3 |
rusqlite, ou sqlx avec SQLite |
|
| Mongoose, le pilote MongoDB | mongodb (le pilote officiel) |
Pas de schémas à la Mongoose ; vous utilisez des structs serde |
| ioredis | redis |
Les bases de données en Rust montre sqlx, Diesel et SeaORM avec du code qui fonctionne.
Les briques du quotidien
Section intitulée « Les briques du quotidien »| JavaScript | Rust | Remarques |
|---|---|---|
JSON.parse / JSON.stringify |
serde + serde_json |
La référence ; gère aussi TOML, YAML, et d’autres formats. Voir JSON avec Serde |
| Zod, Yup | Les types de votre struct + serde font l’essentiel du travail ; validator ou garde pour des règles comme « au moins 8 caractères » |
Si le JSON ne correspond pas à la struct, l’analyse échoue avec une erreur |
| Lodash | Surtout les itérateurs de la bibliothèque standard ; itertools pour des extras comme unique, sorted_by et chunk_by |
|
| date-fns, Day.js, Luxon | chrono ou jiff |
Voir Dates et heures |
| dotenv | dotenvy |
Voir Environnement et processus |
| Commander, yargs | clap |
Voir Projet : outil CLI avec clap |
| chalk | colored ou owo-colors |
|
| winston, pino | tracing (+ tracing-subscriber), ou log + env_logger |
Voir Journalisation |
uuid |
uuid |
|
EventEmitter |
Des canaux (std::sync::mpsc, ou tokio::sync::broadcast pour de nombreux auditeurs) |
Voir Les canaux |
| BullMQ (files de tâches) | tokio::spawn pour des tâches dans le processus ; apalis pour des files de tâches persistantes |
|
| Expressions régulières | regex |
Pas de lookahead ni de références arrière, par choix. Voir Expressions régulières |
Frontend, desktop et Node.js
Section intitulée « Frontend, desktop et Node.js »| JavaScript | Rust | Remarques |
|---|---|---|
| Accélérer du code lourd dans le navigateur | Compiler du Rust en WebAssembly avec wasm-bindgen et wasm-pack |
Voir Rust et WebAssembly et la section plus bas |
| React, Vue, Svelte | Leptos, Yew, Dioxus (tous compilés en WebAssembly) |
Tous avant la version 1.0 ; les API changent encore |
| Electron | Tauri : votre frontend web + un backend Rust, avec des applications bien plus légères |
Tauri 2 est stable. Voir Applications graphiques |
| Modules natifs Node.js (C++ avec node-gyp) | napi + napi-derive (napi-rs), ou neon |
Publiez des binaires précompilés sur npm ; pas besoin de C++ |
| Jeux avec Phaser, Three.js | Bevy, macroquad |
Bevy est avant la 1.0 et casse son API d’une version à l’autre. Voir Développement de jeux |
Côte à côte : une interface devient une struct
Section intitulée « Côte à côte : une interface devient une struct »Voici un objet TypeScript avec une interface, une fonction qui joue le rôle de méthode, et une copie avec spread :
interface User { name: string; age: number; admin: boolean;}
const greeting = (u: User) => `Bonjour, je suis ${u.name} (${u.age} ans)`;
const ada: User = { name: "Ada", age: 36, admin: false };const promoted = { ...ada, admin: true };
console.log(greeting(ada));console.log(promoted);En Rust, la forme devient une struct, et la fonction devient une méthode dans un bloc impl :
#[derive(Debug, Clone)]struct User { name: String, age: u32, admin: bool,}
impl User { // `&self` veut dire « cette méthode lit l'utilisateur, elle ne le modifie pas ». fn greeting(&self) -> String { format!("Bonjour, je suis {} ({} ans)", self.name, self.age) }}
fn main() { let ada = User { name: "Ada".to_string(), age: 36, admin: false };
// Comme { ...ada, admin: true } en JavaScript. let promoted = User { admin: true, ..ada.clone() };
println!("{}", ada.greeting()); println!("{:?}", promoted);}Sortie :
Bonjour, je suis Ada (36 ans)User { name: "Ada", age: 36, admin: true }Quelques différences sautent aux yeux. Chaque champ a un type précis : age est un u32 (un nombre entier qui ne peut pas être négatif), pas un number générique. C’est #[derive(Debug)] qui vous permet d’afficher toute la struct avec {:?}, comme console.log(obj). Et pourquoi .clone() ? Le spread ..ada déplacerait la chaîne name hors de ada, et on ne pourrait plus utiliser ada ensuite. Cloner d’abord garde ada intact. C’est l’ownership à l’œuvre, et vous le croiserez souvent.
Côte à côte : une union discriminée devient un enum
Section intitulée « Côte à côte : une union discriminée devient un enum »Les développeurs TypeScript adorent les unions discriminées : un champ kind indique quelle forme vous avez, et switch affine le type :
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));Rust intègre ça directement, et c’est l’une des meilleures fonctionnalités du langage. Chaque variante d’un enum porte ses propres données, et match les décompose :
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)); }}Sortie :
3.146.00Pas besoin d’une chaîne kind : la variante elle-même sert d’étiquette. Et match doit couvrir toutes les variantes. Si vous ajoutez Triangle plus tard, chaque match qui l’oublie devient une erreur de compilation : c’est comme l’astuce d’exhaustivité avec never en TypeScript, mais toujours active. Plus de détails dans Le pattern matching.
Côte à côte : undefined et null deviennent Option
Section intitulée « Côte à côte : undefined et null deviennent Option »En TypeScript, les valeurs optionnelles apparaissent sous forme d’undefined, de null ou de propriété optionnelle, et vous utilisez ?. et ?? pour les gérer :
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() ?? "pas d'e-mail"}`);}En Rust, une valeur qui peut manquer a le type Option<T>. C’est soit Some(value), soit None, et vous ne pouvez pas l’utiliser comme une valeur ordinaire tant que vous n’avez pas traité le cas 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, }}
// Dans une fonction qui renvoie une Option, `?` fonctionne comme `?.` :// si la valeur est None, on s'arrête là et on renvoie 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] { // Comme findUser(id)?.email?.toUpperCase() ?? "pas d'e-mail" let email = find_user(id) .and_then(|user| user.email) .map(|email| email.to_uppercase()) .unwrap_or("pas d'e-mail".to_string()); println!("{id} : {email}"); }
println!("{:?}", email_domain(1)); println!("{:?}", email_domain(2));
if let Some(user) = find_user(2) { println!("trouvé : {}", user.name); }}Sortie :
2 : pas d'e-mail3 : pas d'e-mailSome("example.com")Nonetrouvé : GraceIl n’y a pas de « cannot read properties of undefined » en Rust, parce qu’il n’y a pas d’undefined à lire. Le compilateur refuse tout simplement que vous appeliez .to_uppercase() sur une Option<String>. Et plus besoin de retenir la différence entre null et undefined : il n’y a que None. Découvrez les méthodes utiles sur la page Option, et les formes courtes sur If Let / While Let.
Côte à côte : try/catch devient Result et ?
Section intitulée « Côte à côte : try/catch devient Result et ? »En JavaScript, une fonction lève une erreur, et vous l’attrapez quelque part plus haut dans la pile d’appels. Rien dans la signature de la fonction ne vous dit qu’elle peut lever une erreur :
function parseAge(input) { const age = Number(input); if (!Number.isInteger(age)) throw new Error(`'${input}' n'est pas un nombre entier`); if (age > 150) throw new Error(`${age} est un âge trop élevé`); 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(`Erreur : ${e.message}`); }}En Rust, une fonction qui peut échouer renvoie un Result : soit Ok(value), soit Err(error). L’opérateur ? veut dire « si ça a échoué, renvoie l’erreur à mon appelant tout de suite ». Il fait le même travail qu’une exception qui remonte, mais vous le voyez dans le code :
fn parse_age(input: &str) -> Result<u32, String> { let age: u32 = input .parse() .map_err(|_| format!("'{input}' n'est pas un nombre entier"))?;
if age > 150 { return Err(format!("{age} est un âge trop élevé")); } Ok(age)}
fn total_age(a: &str, b: &str) -> Result<u32, String> { // Chaque `?` fait remonter l'erreur à notre appelant, comme le ferait une exception. 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!("Total : {total}"), Err(e) => println!("Erreur : {e}"), } }}Sortie :
Total : 77Erreur : 'abc' n'est pas un nombre entierErreur : 200 est un âge trop élevéLa signature -> Result<u32, String> annonce à chaque appelant « ceci peut échouer », et le compilateur ne le laisse pas l’ignorer. C’est pareil pour le code async : là où vous auriez une Promise rejetée en JavaScript, une fonction async Rust renvoie un Result, et vous écrivez .await?. Pour vos propres types d’erreur, voyez Erreurs personnalisées. Dans les applications, la crate anyhow vous donne quelque chose de proche de « attraper n’importe quelle erreur » (voir anyhow et thiserror).
Côte à côte : les méthodes de tableau deviennent des itérateurs
Section intitulée « Côte à côte : les méthodes de tableau deviennent des itérateurs »Les méthodes de tableau que vous utilisez tous les jours ont des équivalents directs en 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(`Chiffre d'affaires : ${revenue}`);console.log(`Première grosse commande : ${firstBig?.customer ?? "aucune"}`);console.log(`Une commande impayée ? ${orders.some((o) => !o.paid)}`);La version Rust utilise des itérateurs et des closures. La syntaxe des closures est |o| o.paid au lieu de (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, puis collect() transforme le résultat en Vec. let paid_names: Vec<&str> = orders .iter() .filter(|o| o.paid) .map(|o| o.customer.as_str()) .collect();
// reduce avec une valeur de départ s'appelle fold. let revenue = orders .iter() .filter(|o| o.paid) .fold(0, |sum, o| sum + o.total);
// find renvoie une Option, parce qu'il peut n'y avoir aucun résultat. let first_big = orders.iter().find(|o| o.total > 150);
println!("{}", paid_names.join(", ")); println!("Chiffre d'affaires : {revenue}"); match first_big { Some(o) => println!("Première grosse commande : {}", o.customer), None => println!("Première grosse commande : aucune"), } // some() s'appelle any(). println!("Une commande impayée ? {}", orders.iter().any(|o| !o.paid));}Sortie :
Ada, LinusChiffre d'affaires : 320Première grosse commande : LinusUne commande impayée ? trueIl y a une grande différence avec JavaScript. En JS, chaque méthode de tableau construit tout de suite un nouveau tableau complet. Les itérateurs Rust sont paresseux : filter et map ne font rien tout seuls, et le travail se fait en un seul passage quand vous appelez quelque chose comme collect(), sum(), fold() ou une boucle for. Aucun tableau temporaire n’est créé entre les deux. Pour la somme seule, vous pourriez aussi écrire .map(|o| o.total).sum::<u32>(). Le tri se fait sur un Vec (sort, sort_by), pas sur les itérateurs ; voir Trier et rechercher.
Côte à côte : les Promises deviennent des futures + tokio
Section intitulée « Côte à côte : les Promises deviennent des futures + tokio »Voici un peu de JavaScript async. Regardez à quel moment "chargement de 1" s’affiche :
async function fetchUser(id) { console.log(`chargement de ${id}`); await new Promise((resolve) => setTimeout(resolve, 100)); return `utilisateur ${id}`;}
const promise = fetchUser(1); // affiche "chargement de 1" tout de suiteconsole.log("promise créée");console.log(await promise);
const [a, b] = await Promise.all([fetchUser(2), fetchUser(3)]);console.log(`${a}, ${b}`);En JavaScript, appeler une fonction async la démarre immédiatement. Rust est différent : appeler une async fn crée seulement un future, une description du travail à faire. Rien ne s’exécute tant que vous ne l’attendez pas avec .await. Et Rust n’a pas de boucle d’événements intégrée : vous ajoutez donc un runtime, et tokio est le plus populaire.
use std::time::Duration;
async fn fetch_user(id: u32) -> String { println!("chargement de {id}"); tokio::time::sleep(Duration::from_millis(100)).await; format!("utilisateur {id}")}
#[tokio::main]async fn main() { // Créer un future ne le démarre PAS. Rien n'est encore affiché. let future = fetch_user(1); println!("future créé"); println!("{}", future.await);
// Comme Promise.all : lancer les deux en même temps et attendre les deux. let (a, b) = tokio::join!(fetch_user(2), fetch_user(3)); println!("{a}, {b}");}Sortie :
future crééchargement de 1utilisateur 1chargement de 2chargement de 3utilisateur 2, utilisateur 3Vous voyez l’ordre ? En JavaScript, « chargement de 1 » s’afficherait avant « promise créée ». En Rust, il arrive après « future créé », parce que le future ne démarre qu’au .await. #[tokio::main] met en place le runtime (votre boucle d’événements) et y exécute main. tokio::join! attend les deux futures ensemble, comme Promise.all ; tokio::spawn lance une tâche en arrière-plan, ce qui se rapproche le plus d’un appel de fonction async sans await. Les leçons Async/Await et Async en profondeur expliquent les détails.
Rust dans votre projet JavaScript : WebAssembly et addons Node
Section intitulée « Rust dans votre projet JavaScript : WebAssembly et addons Node »Pas besoin de tout réécrire pour commencer à utiliser Rust. Voici deux façons courantes de l’intégrer à un projet JavaScript :
WebAssembly dans le navigateur. Marquez des fonctions Rust avec #[wasm_bindgen], compilez avec wasm-pack, et importez le résultat comme n’importe quel module ES. wasm-pack écrit même un fichier .d.ts, pour que TypeScript connaisse les types de vos fonctions :
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(); // télécharge et démarre le fichier .wasmconsole.log(slugify("Hello Rust World")); // "hello-rust-world"La leçon Rust et WebAssembly vous guide dans toute l’installation, et explique quelles valeurs passent la frontière à moindre coût. Un conseil : Wasm est rentable pour les calculs lourds (traitement d’images, analyse syntaxique, simulations), pas pour de petites fonctions appelées des milliers de fois, parce que chaque appel entre JS et Wasm a un petit coût.
Modules natifs Node.js. Avec napi-rs (les crates napi et napi-derive), vous marquez une fonction Rust avec #[napi], et elle devient une fonction que vous pouvez charger avec require ou import depuis Node. Beaucoup de paquets npm populaires publient ainsi des binaires Rust précompilés : leurs utilisateurs n’ont jamais besoin d’un compilateur Rust.
Les pièges quand on vient de JavaScript
Section intitulée « Les pièges quand on vient de JavaScript »Tous les développeurs qui viennent de JavaScript butent sur les mêmes obstacles. Les connaître à l’avance aide.
Passer une valeur à une fonction la déplace. En JavaScript, passer un tableau à une fonction passe une référence, et vous pouvez continuer à utiliser le tableau ensuite. En Rust, passer un Vec ou une String par valeur déplace (move) l’ownership dans la fonction, et la variable d’origine ne peut plus être utilisée :
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 moveLe compilateur vous dit (en anglais) que names a été déplacé dans print_all à la ligne 9, puis emprunté à la ligne 10 alors qu’il n’est plus valide. La solution habituelle est d’emprunter : faites prendre names: &[String] à la fonction et appelez-la avec print_all(&names). Lisez d’abord L’ownership et Le borrowing ; ces pages expliquent cette règle, et vous la croiserez tous les jours.
Les nombres ont des types, et ils ne se mélangent pas. JavaScript a un seul type number. En Rust, 3 est un entier et 2.5 un flottant, et vous ne pouvez pas les combiner sans conversion :
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}`Le message dit (en anglais) qu’on ne peut pas multiplier un entier par un flottant. Écrivez items as f64 * price, ou faites de items un flottant dès le départ (3.0). Notez aussi que la division entière laisse tomber le reste : 7 / 2 vaut 3, pas 3.5. Voir Les types de données et Conversion de types.
Quelques autres pièges que vous rencontrerez :
- Les futures sont paresseux et ont besoin d’un runtime. Comme vous l’avez vu plus haut, rien ne se passe avant
.await, et il vous faut tokio (ou un autre runtime) pour exécuter du code async. Oublier.awaitvous donne un avertissement du compilateur, pas une tâche qui tourne. - On ne peut pas indexer une chaîne.
name[0]ne compile pas, parce qu’uneStringRust est en UTF-8 et qu’un caractère peut occuper plusieurs octets. Utilisezname.chars().next()ou.chars().nth(i). Et apprenez tôt la différence entreStringet&str: les fonctions prennent en général un&str, les structs stockent en général uneString. Voir Les chaînes. - Pas de typage structurel ni de
any. En TypeScript, n’importe quel objet qui a les bons champs correspond à une interface. En Rust, une fonction qui prend unUsern’accepte qu’unUser. Pour un comportement commun, utilisez des traits. Pour du JSON dont vous ne connaissez pas la forme, utilisezserde_json::Value. - Les graphes d’objets partagés et modifiables sont difficiles. En JavaScript, il est normal de faire circuler un objet et de laisser tout le monde le modifier, ou d’avoir des objets qui pointent vers leur parent. Le borrow checker autorise plusieurs lecteurs ou un seul rédacteur à la fois, jamais les deux. Ce genre de schéma demande des pointeurs intelligents (
Rc<RefCell<T>>,Arc<Mutex<T>>) ou, mieux, une conception différente. Attendez-vous à quelques semaines frustrantes. Ça devient beaucoup plus facile ensuite. - Une
HashMapn’a pas d’ordre. LesMapet les objets JavaScript gardent l’ordre d’insertion, et le code en dépend souvent sans qu’on s’en rende compte. UneHashMapRust, non. Triez avant d’afficher, ou utilisez uneBTreeMapou la crateindexmap. - Pas d’exceptions. Les erreurs sont des valeurs, et vous écrirez souvent
?. Appeler.unwrap()est acceptable pendant l’apprentissage, mais c’est la version Rust du!de TypeScript : « faites-moi confiance, la valeur est là ». Si vous vous trompez, le programme part en panic. - Les temps de compilation. Il n’y a pas de rechargement instantané comme avec
node app.js. Une première compilation avec beaucoup de crates prend du temps ; les suivantes sont incrémentales et plus rapides. Utilisezcargo checkpendant que vous modifiez votre code, puisqu’il saute la génération de code.
Un parcours d’apprentissage pour les développeurs JavaScript
Section intitulée « Un parcours d’apprentissage pour les développeurs JavaScript »Vous n’êtes pas obligé de lire ce site dans l’ordre. En tant que développeur JavaScript ou TypeScript, ce parcours saute ce que vous savez déjà et prend son temps là où Rust est différent :
- Débuter avec Rust et Les outils de Rust : installez Rust, configurez VS Code avec rust-analyzer, lancez
cargo new. - Les variables et Les types de données : lisez-les attentivement. Les types numériques sont nouveaux pour vous.
- L’ownership, puis Le borrowing, puis Les slices : les pages les plus importantes pour vous. Prenez votre temps.
- Les chaînes :
Stringcontre&str. - Les structs et Les enums, puis Match et Le pattern matching : vos interfaces et vos unions discriminées.
- Option et Result, puis Erreurs personnalisées.
- Les closures et Les itérateurs : vos méthodes de tableau.
- Les vecteurs, HashMap et HashSet.
- Les traits, Les génériques et Les traits courants.
- Les modules, Cargo et les crates et Les tests.
- Les lifetimes et Pointeurs intelligents.
- Async/Await et Async en profondeur.
- JSON avec Serde, puis construisez le projet d’API web, ou essayez WebAssembly.
Exercices
Section intitulée « Exercices »Les deux exercices vous donnent un extrait de code JavaScript ou TypeScript. Portez-le en Rust.
1. Compter des mots avec une Map
Section intitulée « 1. Compter des mots avec une Map »Ce code JavaScript compte combien de fois chaque mot apparaît, puis affiche les mots du plus fréquent au moins fréquent (en cas d’égalité, par ordre alphabétique). Portez-le en Rust. Indice : utilisez une HashMap<&str, u32> et *counts.entry(word).or_insert(0) += 1. Pour trier, collectez la map dans un Vec de paires (mot, nombre) et utilisez 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}`);Voir la solution
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; }
// Comme [...counts] : transformer la map en liste de paires (mot, nombre). let mut sorted: Vec<(&str, u32)> = counts.into_iter().collect(); // Les plus fréquents d'abord ; en cas d'égalité, par ordre alphabétique. sorted.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (word, n) in sorted { println!("{word}: {n}"); }}Sortie :
the: 3and: 2bat: 1cat: 1hat: 1entry(word).or_insert(0) fait en une seule étape la partie counts.get(word) ?? 0 et le set : il vous donne une référence mutable vers le compteur, en le créant avec 0 s’il n’existe pas. Le tri est indispensable ici, pas seulement agréable : sans lui, une HashMap s’affiche dans un ordre différent d’une exécution à l’autre. La phrase reste en anglais parce que l’exemple trie les mots par ordre alphabétique.
2. Porter des options avec des valeurs par défaut
Section intitulée « 2. Porter des options avec des valeurs par défaut »Cette fonction TypeScript prend un objet d’options dont tous les champs sont optionnels, et complète les valeurs manquantes avec ??. Portez-la en Rust. Indice : créez une struct avec des champs Option et ajoutez #[derive(Default)]. Ensuite, ServerOptions { port: Some(3000), ..Default::default() } fonctionne comme le fait de passer { port: 3000 }. Pour un bool, la valeur par défaut est 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 ? "bavard" : "silencieux"; return `${host}:${port} (${mode})`;}
console.log(describe({}));console.log(describe({ port: 3000 }));console.log(describe({ host: "example.com", verbose: true }));Voir la solution
#[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 { "bavard" } else { "silencieux" }; 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() }) );}Sortie :
localhost:8080 (silencieux)localhost:3000 (silencieux)example.com:8080 (bavard)unwrap_or est votre ??. as_deref() transforme une Option<String> en Option<&str>, pour pouvoir se rabattre sur le simple texte "localhost" sans rien copier. Et ..Default::default() est la façon Rust de dire « tous les champs que je n’ai pas mentionnés prennent leur valeur par défaut », ce qui correspond exactement à l’omission d’une propriété en JavaScript. Un port de type u16 veut aussi dire qu’un port comme 70000 ne peut même pas être stocké par erreur.
Récapitulatif
Section intitulée « Récapitulatif »- Une grande partie de JavaScript et TypeScript se retrouve en Rust : les closures, async/await, la déstructuration et les méthodes à la manière des tableaux comme
map,filteretfindont toutes des équivalents proches. - Les grands changements : l’ownership au lieu d’un ramasse-miettes,
Optionau lieu d’undefinedetnull, etResult+?au lieu dethrowettry/catch. - Les interfaces TypeScript deviennent des structs (pour les données) ou des traits (pour le comportement), et les unions discriminées deviennent des enums avec
match. - Les futures Rust sont paresseux et ont besoin d’un runtime comme tokio ;
tokio::join!joue le rôle dePromise.all. - Cargo remplace npm,
package.jsonet vos scripts ; clippy et rustfmt remplacent ESLint et Prettier ; les tests sont intégrés. - Pour Express, regardez axum ou actix-web ; pour
fetch, reqwest ; pour Prisma, SeaORM ou Diesel ; et pour Electron, Tauri. Vous pouvez aussi faire entrer Rust dans un projet JS grâce à WebAssembly ou napi-rs.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Qu'est-ce que Rust utilise à la place d'undefined et null ?
2. En JavaScript, appeler une fonction async la démarre tout de suite. Que se passe-t-il quand vous appelez une async fn en Rust sans .await ?
3. Votre code TypeScript utilise une union discriminée avec un champ kind. Quelle fonctionnalité de Rust s'en rapproche le plus ?
4. Le backend de votre équipe utilise Express. Quelle crate Rust s'en rapproche le plus ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- The Rust Programming Language (en anglais) : le livre officiel. Les chapitres 4 (ownership), 6 (enums et pattern matching) et 9 (erreurs) sont les plus importants pour les développeurs JavaScript.
- Tokio tutorial (en anglais) : le guide officiel de l’async en Rust avec tokio, de votre première tâche jusqu’aux canaux et aux entrées-sorties.
- The wasm-bindgen Guide (en anglais) : comment Rust et JavaScript communiquent en WebAssembly, avec de nombreux exemples.
- Compiling from Rust to WebAssembly (MDN) (en anglais) : un tutoriel pas à pas de MDN Web Docs, publication sur npm comprise.
- napi-rs (en anglais) : la documentation pour écrire des addons Node.js en Rust.