Rust für JavaScript- und TypeScript-Entwickler
Wenn du JavaScript oder TypeScript schreibst, kannst du schon mehr Rust, als du denkst. Rust hat Closures, die ganz ähnlich funktionieren wie Arrow Functions, async/await, Iterator-Methoden mit bekannten Namen wie map, filter und find, Destructuring und ein Kommandozeilen-Tool (Cargo), das erledigt, was npm für dich tut. Wenn du TypeScript nutzt, kennst du auch schon einen Compiler, der Fehler findet, bevor dein Code läuft. Der Compiler von Rust macht dasselbe, nur strenger.
Was ist also wirklich neu? Drei Dinge. Rust hat keinen Garbage Collector: Jeder Wert hat genau einen Besitzer, und der Compiler prüft Ownership (Eigentum) und Borrowing (Ausleihen), bevor dein Programm läuft. Rust hat kein undefined und kein null: Ein Wert, der fehlen könnte, hat den Typ Option. Und Rust hat keine Exceptions: Eine Funktion, die fehlschlagen kann, gibt ein Result zurück. In diese drei Ideen fließt der Großteil deiner Lernzeit.
Warum lernen JavaScript-Entwickler Rust? Meist aus einem dieser Gründe: Du willst ein schnelles Web-Backend oder Kommandozeilen-Tool, das als einzelne Datei ausgeliefert wird. Du willst eine heiße Stelle im Browser mit WebAssembly beschleunigen. Du willst ein natives Node.js-Addon schreiben. Oder du bist neugierig auf die Tools, die du sowieso schon nutzt: SWC, Biome, Oxc, Rolldown, Rspack, Turbopack, Lightning CSS und Deno sind alle in Rust geschrieben.
Diese Seite übersetzt das, was du aus JavaScript und TypeScript kennst, in Rust. So kannst du deine Zeit in die Teile stecken, die wirklich neu sind. Wenn du lieber einen allgemeinen Vergleich mit C++, Go und Python suchst, schau unter Rust vs. C++, Go und Python.
Sprachkonzepte: von JavaScript und TypeScript zu Rust
Abschnitt betitelt „Sprachkonzepte: von JavaScript und TypeScript zu Rust“Nutze diese Tabelle wie einen Sprachführer. Die letzte Spalte verlinkt die Lektion auf dieser Seite, die die Rust-Seite erklärt.
| JavaScript / TypeScript | Rust | Mehr dazu |
|---|---|---|
let und const |
let (unveränderbar) und let mut (veränderbar). Ein const-Objekt in JS kann innen trotzdem geändert werden, ein let-Wert in Rust nicht |
Variablen |
number |
Du wählst eine Größe: i32, i64, u32, usize, f64, … Ganzzahlen und Gleitkommazahlen sind verschiedene Typen |
Datentypen |
bigint |
i128 / u128 oder das Crate num-bigint für beliebige Größen |
Datentypen |
string |
String (besessen, wachsend) und &str (eine geliehene Sicht auf Text) |
Strings |
boolean |
bool |
Booleans |
| Template Literals | format!("Hi {name}") und println!("Hi {name}") |
Ausgabe |
any, unknown |
Kein any. Nutze Generics, ein Enum oder serde_json::Value für JSON mit unbekannter Struktur |
Generics, JSON mit Serde |
interface oder type, das die Form eines Objekts beschreibt |
struct |
Structs |
interface, das Methoden beschreibt |
trait |
Traits |
| Strukturelle Typisierung (gleiche Form = gleicher Typ) | Nominale Typisierung: Zwei Structs mit denselben Feldern sind trotzdem verschiedene Typen | Structs |
| Union Types und Discriminated Unions | enum, bei dem jede Variante eigene Daten tragen kann |
Enums |
String-Literal-Typen wie "small" oder "large" |
Ein enum mit einfachen Varianten: enum Size { Small, Large } |
Enums |
undefined, null, optionale Property x?: T |
Option<T>: entweder Some(value) oder None |
Option |
?. und ?? |
? (in einer Funktion, die Option zurückgibt), .map(...), .and_then(...), .unwrap_or(...) |
Option |
Non-Null-Assertion x! |
.unwrap() oder .expect("Grund"), die das Programm beenden, wenn der Wert None ist |
Panic |
throw, try/catch, abgelehnte Promises |
Result<T, E> + der Operator ? |
Result |
class MyError extends Error |
Ein eigenes Fehler-Enum (oft mit thiserror); in Anwendungen anyhow |
Eigene Fehlertypen, anyhow und thiserror |
class mit Feldern und Methoden |
struct für die Daten + ein impl-Block für die Methoden |
Structs |
extends (Vererbung) |
Keine Vererbung. Nutze Komposition (ein Struct in einem Struct) + Traits mit Standardmethoden | Traits, Entwurfsmuster |
class Dog implements Animal |
impl Animal for Dog |
Traits |
| Ein Array mit verschiedenen Klassen, die ein Interface teilen | Vec<Box<dyn Animal>> (ein Trait-Objekt) |
Trait-Objekte |
Generics wie T extends HasId |
Generics mit Trait Bounds: T: HasId |
Generics |
| Arrow Functions und Callbacks | Closures, beschrieben durch die Traits Fn, FnMut, FnOnce |
Closures, Fn, FnMut und FnOnce |
this |
Ein expliziter Parameter self, &self oder &mut self. Keine Überraschungen, worauf this gerade zeigt |
Structs |
Destructuring const { x, y } = point |
Patterns: let Point { x, y } = point; (auch in match und in Funktionsparametern) |
Pattern Matching |
Spread { ...user, admin: true } |
Struct-Update-Syntax: User { admin: true, ..user } |
Structs |
switch oder if-Ketten über typeof / kind |
match (der Compiler prüft, ob du jeden Fall abgedeckt hast) |
Match |
=== |
==, das Werte vergleicht (über den Trait PartialEq). Niemals automatische Typumwandlung |
Wichtige Traits |
for...of |
for item in &items |
For-Schleifen |
map, filter, reduce, find, some, every |
Iterator-Methoden: map, filter, fold, find, any, all, danach collect |
Iteratoren |
Generatoren (function*) und Symbol.iterator |
Den Trait Iterator implementieren |
Eigene Iteratoren |
Promise<T> |
Ein Future mit Output = T. Anders als ein Promise tut es nichts, bis es awaited wird |
Async/Await |
async / await |
async fn und .await, dazu eine Runtime wie tokio, die sie ausführt |
Async/Await, Async im Detail |
Promise.all |
tokio::join! oder join_all aus dem Crate futures |
Async im Detail |
setTimeout |
tokio::time::sleep in async-Code, sonst std::thread::sleep |
Async/Await |
Web Workers, worker_threads |
Echte Threads: std::thread::spawn oder rayon für parallele Schleifen |
Nebenläufigkeit |
ES-Module: import, export |
Module: mod, use und pub |
Module |
JSON.parse / JSON.stringify |
serde_json mit #[derive(Serialize, Deserialize)] |
JSON mit Serde |
console.log |
println!, und dbg! zum schnellen Debuggen. Nutze {:?} für Typen mit #[derive(Debug)] |
Ausgabe |
readonly, as const, Object.freeze |
Nicht nötig: Alles ist schreibgeschützt, solange du nicht mut schreibst |
Variablen |
| Decorators | Attribute wie #[derive(Debug)] und Makros |
Makros |
Number("abc") ergibt NaN |
"abc".parse::<i32>() gibt ein Err zurück, das du behandeln musst |
Typumwandlung, Konvertierungen |
| Garbage Collector | Ownership und Borrowing, geprüft beim Kompilieren | Ownership, Borrowing |
Collections
Abschnitt betitelt „Collections“| JavaScript | Rust | Mehr dazu |
|---|---|---|
Array |
Vec<T> (jedes Element hat denselben Typ) |
Vektoren |
Array mit fester Länge oder ein TS-Tupel [string, number] |
[T; N] für eine feste Länge, ein Tupel (String, i32) für gemischte Typen |
Arrays, Tupel |
Map oder ein Objekt als Dictionary (Record<string, T>) |
HashMap<K, V> |
HashMap |
| Eine Map mit sortierten Schlüsseln | BTreeMap<K, V> |
HashMap |
Set |
HashSet<T> (oder BTreeSet<T> für sortierte Reihenfolge) |
HashSet |
Array als Queue (push + shift) |
VecDeque<T> (push_back / pop_front, beide schnell) |
VecDeque und BinaryHeap |
Array als Stack (push + pop) |
Vec<T> mit push / pop |
Vektoren |
Uint8Array, Buffer |
Vec<u8> oder ein Byte-Slice &[u8] |
Slices |
WeakMap, WeakRef |
Weak<T> (aus Rc oder Arc) |
Weak und Cow |
Ein Punkt, auf den du achten solltest: Map, Set und einfache Objekte in JavaScript merken sich die Reihenfolge, in der du Dinge eingefügt hast. HashMap und HashSet in Rust tun das nicht. Wenn du eine sortierte Reihenfolge brauchst, nimm BTreeMap. Wenn du die Einfügereihenfolge brauchst, gibt dir das Crate indexmap genau das.
Werkzeuge: von npm zu cargo
Abschnitt betitelt „Werkzeuge: von npm zu cargo“Mit Cargo wirst du dich schnell zu Hause fühlen. Es ist Paketmanager, Build-Tool, Test-Runner und Script-Runner in einem, und jedes Rust-Projekt nutzt es auf dieselbe Weise.
| JavaScript | Rust | Hinweise |
|---|---|---|
node + npm (oder pnpm, yarn) |
cargo |
Baut, startet, testet, fügt Pakete hinzu, veröffentlicht. Siehe Cargo und Crates |
package.json |
Cargo.toml |
|
package-lock.json |
Cargo.lock |
Wird für dich erzeugt |
node_modules/ |
Kein Ordner pro Projekt. Downloads landen einmal im Cache unter ~/.cargo, Build-Ergebnisse in target/ |
|
| npmjs.com | crates.io | Die API-Doku jedes veröffentlichten Crates steht auf docs.rs |
npm install zod |
cargo add serde |
|
npm init |
cargo new my_app (cargo new --lib für eine Bibliothek) |
|
npm run dev / build / test |
cargo run / cargo build / cargo test |
Mit --release bekommst du einen optimierten Build; Debug-Builds sind deutlich langsamer |
"scripts" in der package.json |
Kein Scripts-Abschnitt. Nutze Cargo-Aliase in .cargo/config.toml oder einen Task-Runner wie just |
|
npx some-tool |
cargo install some-tool, dann ausführen |
cargo binstall lädt fertige Binärdateien herunter, statt zu kompilieren |
nvm und .nvmrc |
rustup und eine Datei rust-toolchain.toml |
Siehe Rust-Werkzeuge und Einrichtung |
| npm- / pnpm-Workspaces (Monorepo) | Ein Cargo-Workspace | Eine Cargo.toml ganz oben listet die zugehörigen Crates auf |
tsc |
rustc, von cargo für dich aufgerufen |
|
tsc --noEmit |
cargo check |
Prüft Typen, ohne zu bauen; viel schneller |
| ESLint | cargo clippy |
Hunderte hilfreiche Lints, offiziell |
| Prettier | cargo fmt |
Ein einheitlicher Stil für alle |
nodemon, tsx watch |
cargo watch oder bacon |
Installiert mit cargo install |
| Jest / Vitest / Mocha | Eingebautes #[test] und cargo test (cargo nextest ist ein beliebter, schnellerer Runner) |
Siehe Testen |
| Jest-Snapshots | insta |
Siehe Testen für Fortgeschrittene |
jest.mock() |
mockall oder handgeschriebene Fakes, die deinen Trait implementieren |
|
| fast-check | proptest |
Property-based Testing |
| c8 / Istanbul (Coverage) | cargo llvm-cov |
|
| JSDoc / TypeDoc | ///-Doc-Kommentare in Markdown + cargo doc |
Code-Beispiele in der Doku laufen als Tests. Siehe Dokumentation |
| VS Code + der TypeScript-Server | VS Code + rust-analyzer oder JetBrains RustRover | rust-analyzer bietet Vervollständigung, Sprung zur Definition und Fehler direkt im Code |
npm publish |
cargo publish |
Hier ist eine kleine Cargo.toml, damit du sie mit einer package.json vergleichen kannst:
[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"] }Eine Version wie "1" bedeutet dasselbe wie "^1" in der package.json: jedes kompatible 1.x-Release. Features sind optionale Teile eines Crates, die du einschaltest; siehe Cargo-Features und cfg.
Bibliotheken und Frameworks: „Was nehmen wir statt …?“
Abschnitt betitelt „Bibliotheken und Frameworks: „Was nehmen wir statt …?““Das fragt jeder JavaScript-Entwickler zuerst: „Wir nutzen Express und Prisma. Was gibt es dafür in Rust?“ Hier sind die nächsten Entsprechungen. Wie bei npm gibt es auf crates.io für die meisten Aufgaben viele Optionen. Diese hier sind beliebt und gut gepflegt.
Web und Netzwerk
Abschnitt betitelt „Web und Netzwerk“| JavaScript | Rust | Hinweise |
|---|---|---|
| Express, Fastify, Koa | axum (wir bauen eine API in Projekt: Web-API mit Axum), actix-web, Rocket | axum und actix-web sind in Produktion weit verbreitet |
| Express-Middleware | tower-Layer und das Crate tower-http (CORS, Kompression, Tracing, Timeouts) |
axum nutzt tower direkt |
fetch, axios |
reqwest |
Standardmäßig async, mit optionalem blockierendem Client. Siehe Netzwerkprogrammierung |
ws, Socket.IO |
WebSockets in axum (das Feature ws), tokio-tungstenite oder socketioxide (ein Socket.IO-Server) |
|
| Apollo Server (GraphQL) | async-graphql |
|
gRPC (@grpc/grpc-js) |
tonic (mit prost für Protobuf) |
|
| Swagger- / OpenAPI-Generatoren | utoipa |
|
jsonwebtoken |
jsonwebtoken |
Gleicher Name, gleiche Idee |
bcrypt |
argon2 oder bcrypt |
Datenzugriff
Abschnitt betitelt „Datenzugriff“| JavaScript | Rust | Hinweise |
|---|---|---|
| Prisma, TypeORM | SeaORM (async, Entities, Migrationen) oder Diesel (Abfragen werden beim Kompilieren typgeprüft) | SeaORM fühlt sich am ehesten wie Prisma und TypeORM an |
Knex, pg, postgres.js |
sqlx: Du schreibst einfaches SQL, und das Makro query! prüft es beim Kompilieren gegen deine echte Datenbank |
Async, sehr beliebt, kein ORM |
better-sqlite3 |
rusqlite oder sqlx mit SQLite |
|
| Mongoose, der MongoDB-Treiber | mongodb (der offizielle Treiber) |
Keine Schemas im Stil von Mongoose; du nutzt serde-Structs |
| ioredis | redis |
Datenbanken in Rust zeigt sqlx, Diesel und SeaORM mit funktionierendem Code.
Bausteine für den Alltag
Abschnitt betitelt „Bausteine für den Alltag“| JavaScript | Rust | Hinweise |
|---|---|---|
JSON.parse / JSON.stringify |
serde + serde_json |
Der Standard; kann auch TOML, YAML und mehr. Siehe JSON mit Serde |
| Zod, Yup | Die Typen deines Structs + serde erledigen das meiste; validator oder garde für Regeln wie „mindestens 8 Zeichen“ |
Passt das JSON nicht zum Struct, schlägt das Parsen mit einem Fehler fehl |
| Lodash | Meist die Iteratoren der Standardbibliothek; itertools für Extras wie unique, sorted_by und chunk_by |
|
| date-fns, Day.js, Luxon | chrono oder jiff |
Siehe Datum und Uhrzeit |
| dotenv | dotenvy |
Siehe Umgebung und Prozesse |
| Commander, yargs | clap |
Siehe Projekt: CLI-Tool mit clap |
| chalk | colored oder owo-colors |
|
| winston, pino | tracing (+ tracing-subscriber) oder log + env_logger |
Siehe Logging |
uuid |
uuid |
|
EventEmitter |
Channels (std::sync::mpsc oder tokio::sync::broadcast für viele Zuhörer) |
Siehe Channels |
| BullMQ (Job-Queues) | tokio::spawn für Tasks im selben Prozess; apalis für persistente Job-Queues |
|
| Reguläre Ausdrücke | regex |
Kein Lookahead und keine Rückverweise, mit Absicht. Siehe Reguläre Ausdrücke |
Frontend, Desktop und Node.js
Abschnitt betitelt „Frontend, Desktop und Node.js“| JavaScript | Rust | Hinweise |
|---|---|---|
| Rechenintensiven Browser-Code beschleunigen | Rust mit wasm-bindgen und wasm-pack zu WebAssembly kompilieren |
Siehe WebAssembly und den Abschnitt weiter unten |
| React, Vue, Svelte | Leptos, Yew, Dioxus (alle kompilieren zu WebAssembly) |
Alle noch vor Version 1.0; APIs ändern sich noch |
| Electron | Tauri: dein Web-Frontend + ein Rust-Backend, mit viel kleineren Apps |
Tauri 2 ist stabil. Siehe GUI-Apps |
| Native Node.js-Addons (C++ mit node-gyp) | napi + napi-derive (napi-rs) oder neon |
Fertige Binärdateien über npm ausliefern; kein C++ nötig |
| Spiele mit Phaser oder Three.js | Bevy, macroquad |
Bevy ist noch vor 1.0 und ändert seine API zwischen Releases. Siehe Spieleentwicklung |
Im direkten Vergleich: aus einem Interface wird ein Struct
Abschnitt betitelt „Im direkten Vergleich: aus einem Interface wird ein Struct“Hier ist ein TypeScript-Objekt mit einem Interface, einer methodenartigen Funktion und einer Spread-Kopie:
interface User { name: string; age: number; admin: boolean;}
const greeting = (u: User) => `Hallo, ich bin ${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 wird die Form zu einem Struct, und die Funktion wird zu einer Methode in einem impl-Block:
#[derive(Debug, Clone)]struct User { name: String, age: u32, admin: bool,}
impl User { // `&self` heißt: „Diese Methode liest den User nur, sie ändert ihn nicht.“ fn greeting(&self) -> String { format!("Hallo, ich bin {} ({})", self.name, self.age) }}
fn main() { let ada = User { name: "Ada".to_string(), age: 36, admin: false };
// Wie { ...ada, admin: true } in JavaScript. let promoted = User { admin: true, ..ada.clone() };
println!("{}", ada.greeting()); println!("{:?}", promoted);}Ausgabe:
Hallo, ich bin Ada (36)User { name: "Ada", age: 36, admin: true }Ein paar Dinge sind anders. Jedes Feld hat einen exakten Typ: age ist ein u32 (eine ganze Zahl, die nicht negativ sein kann), keine allgemeine number. Erst #[derive(Debug)] erlaubt dir, das ganze Struct mit {:?} auszugeben, so wie console.log(obj). Und warum .clone()? Der Spread ..ada würde den String name aus ada herausverschieben, und danach könnten wir ada nicht mehr benutzen. Mit dem Klon vorher bleibt ada unversehrt. Das ist Ownership bei der Arbeit, und das wirst du oft sehen.
Im direkten Vergleich: aus einer Discriminated Union wird ein Enum
Abschnitt betitelt „Im direkten Vergleich: aus einer Discriminated Union wird ein Enum“TypeScript-Entwickler lieben Discriminated Unions: Ein Feld kind sagt dir, welche Form du vor dir hast, und switch grenzt den Typ ein:
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 hat das eingebaut, und es ist eines der besten Features der Sprache. Jede Variante eines Enums trägt ihre eigenen Daten, und match nimmt sie auseinander:
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)); }}Ausgabe:
3.146.00Du brauchst keinen kind-String: Die Variante selbst ist das Etikett. Und match muss jede Variante abdecken. Wenn du später Triangle hinzufügst, wird jedes match, das es vergisst, zu einem Compilerfehler. Das ist wie der never-Trick für Vollständigkeit in TypeScript, nur immer eingeschaltet. Mehr dazu unter Pattern Matching.
Im direkten Vergleich: aus undefined und null wird Option
Abschnitt betitelt „Im direkten Vergleich: aus undefined und null wird Option“In TypeScript tauchen optionale Werte als undefined, null oder optionale Property auf, und du gehst mit ?. und ?? damit um:
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() ?? "keine E-Mail"}`);}In Rust hat ein Wert, der fehlen könnte, den Typ Option<T>. Er ist entweder Some(value) oder None, und du kannst ihn erst wie einen normalen Wert benutzen, wenn du den Fall None behandelt hast:
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, }}
// In einer Funktion, die Option zurückgibt, funktioniert `?` wie `?.`:// Ist der Wert None, hör hier auf und gib None zurück.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] { // Wie findUser(id)?.email?.toUpperCase() ?? "keine E-Mail" let email = find_user(id) .and_then(|user| user.email) .map(|email| email.to_uppercase()) .unwrap_or("keine E-Mail".to_string()); println!("{id}: {email}"); }
println!("{:?}", email_domain(1)); println!("{:?}", email_domain(2));
if let Some(user) = find_user(2) { println!("gefunden: {}", user.name); }}Ausgabe:
2: keine E-Mail3: keine E-MailSome("example.com")Nonegefunden: GraceIn Rust gibt es kein „cannot read properties of undefined“, weil es kein undefined gibt, aus dem man lesen könnte. Der Compiler lässt dich .to_uppercase() einfach nicht auf einer Option<String> aufrufen. Und du musst dir keinen Unterschied zwischen null und undefined merken: Es gibt nur None. Die Hilfsmethoden lernst du auf der Seite Option kennen, die Kurzformen unter If Let / While Let.
Im direkten Vergleich: aus try/catch wird Result und ?
Abschnitt betitelt „Im direkten Vergleich: aus try/catch wird Result und ?“In JavaScript wirft eine Funktion einen Fehler, und du fängst ihn irgendwo weiter oben im Call Stack ab. Nichts in der Signatur der Funktion verrät dir, dass sie werfen könnte:
function parseAge(input) { const age = Number(input); if (!Number.isInteger(age)) throw new Error(`'${input}' ist keine ganze Zahl`); if (age > 150) throw new Error(`${age} ist zu alt`); 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(`Gesamt: ${totalAge(a, b)}`); } catch (e) { console.log(`Fehler: ${e.message}`); }}In Rust gibt eine Funktion, die fehlschlagen kann, ein Result zurück: entweder Ok(value) oder Err(error). Der Operator ? bedeutet: „Wenn das fehlgeschlagen ist, gib den Fehler sofort an meinen Aufrufer zurück.“ Er erledigt dieselbe Aufgabe, wie eine Exception nach oben durchzureichen – nur siehst du es im Code:
fn parse_age(input: &str) -> Result<u32, String> { let age: u32 = input .parse() .map_err(|_| format!("'{input}' ist keine ganze Zahl"))?;
if age > 150 { return Err(format!("{age} ist zu alt")); } Ok(age)}
fn total_age(a: &str, b: &str) -> Result<u32, String> { // Jedes `?` reicht einen Fehler an unseren Aufrufer weiter, wie es eine Exception tun würde. 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!("Gesamt: {total}"), Err(e) => println!("Fehler: {e}"), } }}Ausgabe:
Gesamt: 77Fehler: 'abc' ist keine ganze ZahlFehler: 200 ist zu altDie Signatur -> Result<u32, String> sagt jedem Aufrufer: „Das kann fehlschlagen.“ Und der Compiler lässt nicht zu, dass er es ignoriert. Dasselbe gilt für async-Code: Wo du in JavaScript ein abgelehntes Promise bekommst, gibt eine async-Funktion in Rust ein Result zurück, und du schreibst .await?. Für eigene Fehlertypen siehe Eigene Fehlertypen. In Anwendungen gibt dir das Crate anyhow etwas, das „fang jeden Fehler“ sehr nahe kommt (siehe anyhow und thiserror).
Im direkten Vergleich: aus Array-Methoden werden Iteratoren
Abschnitt betitelt „Im direkten Vergleich: aus Array-Methoden werden Iteratoren“Die Array-Methoden, die du jeden Tag nutzt, haben direkte Entsprechungen 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(`Umsatz: ${revenue}`);console.log(`Erste große Bestellung: ${firstBig?.customer ?? "keine"}`);console.log(`Noch etwas unbezahlt? ${orders.some((o) => !o.paid)}`);Die Rust-Version nutzt Iteratoren und Closures. Die Closure-Syntax ist |o| o.paid statt (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, danach macht collect() aus dem Ergebnis einen Vec. let paid_names: Vec<&str> = orders .iter() .filter(|o| o.paid) .map(|o| o.customer.as_str()) .collect();
// reduce mit Startwert heißt fold. let revenue = orders .iter() .filter(|o| o.paid) .fold(0, |sum, o| sum + o.total);
// find gibt eine Option zurück, weil es vielleicht keinen Treffer gibt. let first_big = orders.iter().find(|o| o.total > 150);
println!("{}", paid_names.join(", ")); println!("Umsatz: {revenue}"); match first_big { Some(o) => println!("Erste große Bestellung: {}", o.customer), None => println!("Erste große Bestellung: keine"), } // some() heißt any(). println!("Noch etwas unbezahlt? {}", orders.iter().any(|o| !o.paid));}Ausgabe:
Ada, LinusUmsatz: 320Erste große Bestellung: LinusNoch etwas unbezahlt? trueEs gibt einen großen Unterschied zu JavaScript. Jede Array-Methode in JS baut sofort ein ganz neues Array. Iteratoren in Rust sind lazy (sie arbeiten erst bei Bedarf): filter und map tun allein gar nichts. Die Arbeit passiert in einem einzigen Durchlauf, wenn du etwas wie collect(), sum(), fold() oder eine for-Schleife aufrufst. Dazwischen entstehen keine temporären Arrays. Nur für die Summe könntest du auch .map(|o| o.total).sum::<u32>() schreiben. Sortieren gehört zu Vec (sort, sort_by), nicht zu Iteratoren; siehe Sortieren und Suchen.
Im direkten Vergleich: aus Promises werden Futures + tokio
Abschnitt betitelt „Im direkten Vergleich: aus Promises werden Futures + tokio“Hier ist etwas async-JavaScript. Achte darauf, wann "lade 1" ausgegeben wird:
async function fetchUser(id) { console.log(`lade ${id}`); await new Promise((resolve) => setTimeout(resolve, 100)); return `Benutzer ${id}`;}
const promise = fetchUser(1); // gibt sofort "lade 1" ausconsole.log("Promise erstellt");console.log(await promise);
const [a, b] = await Promise.all([fetchUser(2), fetchUser(3)]);console.log(`${a}, ${b}`);In JavaScript startet der Aufruf einer async-Funktion sie sofort. Rust ist anders: Der Aufruf einer async fn erzeugt nur ein Future, eine Beschreibung der Arbeit. Nichts läuft, bis du es mit .await abwartest. Und Rust hat keine eingebaute Event Loop, deshalb fügst du eine Runtime hinzu: tokio (auf Englisch) ist die beliebteste.
use std::time::Duration;
async fn fetch_user(id: u32) -> String { println!("lade {id}"); tokio::time::sleep(Duration::from_millis(100)).await; format!("Benutzer {id}")}
#[tokio::main]async fn main() { // Ein Future zu erzeugen startet es NICHT. Noch wird nichts ausgegeben. let future = fetch_user(1); println!("Future erstellt"); println!("{}", future.await);
// Wie Promise.all: beide gleichzeitig ausführen und auf beide warten. let (a, b) = tokio::join!(fetch_user(2), fetch_user(3)); println!("{a}, {b}");}Ausgabe:
Future erstelltlade 1Benutzer 1lade 2lade 3Benutzer 2, Benutzer 3Siehst du die Reihenfolge? In JavaScript käme „lade 1“ vor „Promise erstellt“. In Rust kommt es nach „Future erstellt“, weil das Future erst bei .await startet. #[tokio::main] richtet die Runtime (deine Event Loop) ein und führt main darauf aus. tokio::join! wartet auf beide Futures zusammen, wie Promise.all. tokio::spawn startet einen Task im Hintergrund – das kommt dem Aufruf einer async-Funktion ohne await am nächsten. Die Lektionen Async/Await und Async im Detail erklären die Einzelheiten.
Rust in deinem JavaScript-Projekt: WebAssembly und Node-Addons
Abschnitt betitelt „Rust in deinem JavaScript-Projekt: WebAssembly und Node-Addons“Du musst nichts neu schreiben, um mit Rust anzufangen. Zwei übliche Wege, es in ein JavaScript-Projekt einzubauen:
WebAssembly im Browser. Markiere Rust-Funktionen mit #[wasm_bindgen], baue mit wasm-pack und importiere das Ergebnis wie jedes ES-Modul. wasm-pack schreibt sogar eine .d.ts-Datei, damit TypeScript die Typen deiner Funktionen kennt:
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(); // die .wasm-Datei laden und startenconsole.log(slugify("Hello Rust World")); // "hello-rust-world"Die Lektion WebAssembly führt dich durch die ganze Einrichtung und erklärt, welche Werte die Grenze günstig überqueren. Ein Tipp: Wasm lohnt sich für schwere Berechnungen (Bildverarbeitung, Parsen, Simulationen), nicht für kleine Funktionen, die du tausendfach aufrufst. Denn jeder Aufruf zwischen JS und Wasm kostet ein wenig.
Native Node.js-Addons. Mit napi-rs (auf Englisch; die Crates napi und napi-derive) markierst du eine Rust-Funktion mit #[napi], und sie wird zu einer Funktion, die du in Node mit require oder import laden kannst. Viele beliebte npm-Pakete liefern so fertig kompilierte Rust-Binärdateien aus. Deine Nutzer brauchen also nie einen Rust-Compiler.
Stolpersteine für JavaScript-Entwickler
Abschnitt betitelt „Stolpersteine für JavaScript-Entwickler“Alle, die von JavaScript kommen, stolpern über dieselben Dinge. Wenn du sie vorher kennst, hilft das.
Einen Wert an eine Funktion zu übergeben, verschiebt ihn. Wenn du in JavaScript ein Array an eine Funktion übergibst, übergibst du eine Referenz, und du kannst das Array danach weiter benutzen. Wenn du in Rust einen Vec oder String als Wert übergibst, verschiebt das das Ownership in die Funktion (ein Move), und die ursprüngliche Variable ist danach nicht mehr benutzbar:
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 moveDer Compiler sagt auf Englisch: names wird ausgeliehen, nachdem sein Wert schon verschoben wurde. Die übliche Lösung ist, stattdessen auszuleihen: Lass die Funktion names: &[String] entgegennehmen und ruf sie mit print_all(&names) auf. Lies zuerst Ownership und Borrowing. Dort wird diese Regel erklärt, und du begegnest ihr jeden Tag.
Zahlen haben Typen, und die lassen sich nicht mischen. JavaScript hat eine einzige number. In Rust ist 3 eine Ganzzahl und 2.5 eine Gleitkommazahl, und du kannst sie nicht ohne Umwandlung kombinieren:
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}`Der Compiler sagt: Eine Ganzzahl kann nicht mit einer Gleitkommazahl multipliziert werden. Schreib items as f64 * price, oder mach items von Anfang an zu einer Gleitkommazahl (3.0). Beachte auch, dass Ganzzahldivision den Rest wegwirft: 7 / 2 ist 3, nicht 3.5. Siehe Datentypen und Typumwandlung.
Ein paar weitere, über die du stolpern wirst:
- Futures sind lazy und brauchen eine Runtime. Wie du oben gesehen hast, passiert nichts bis
.await, und du brauchst tokio (oder eine andere Runtime), um überhaupt async-Code auszuführen. Ein vergessenes.awaitbringt dir eine Compiler-Warnung, keinen laufenden Task. - Strings lassen sich nicht indizieren.
name[0]kompiliert nicht, weil einStringin Rust UTF-8 ist und ein Zeichen mehrere Bytes belegen kann. Nutzename.chars().next()oder.chars().nth(i). Und lerne früh den Unterschied zwischenStringund&str: Funktionen nehmen meist&strentgegen, Structs speichern meistString. Siehe Strings. - Keine strukturelle Typisierung und kein
any. In TypeScript passt jedes Objekt mit den richtigen Feldern zu einem Interface. In Rust nimmt eine Funktion, die einenUsererwartet, nur einenUser. Für gemeinsames Verhalten nutzt du Traits. Für JSON, dessen Struktur du nicht kennst, nutzt duserde_json::Value. - Geteilte, veränderbare Objektgraphen sind schwierig. In JavaScript ist es normal, ein Objekt herumzureichen und alle es ändern zu lassen, oder Objekte auf ihr Elternobjekt zurückzeigen zu lassen. Der Borrow Checker erlaubt viele Leser oder genau einen Schreiber gleichzeitig, nie beides. Solche Muster brauchen Smart Pointer (
Rc<RefCell<T>>,Arc<Mutex<T>>) oder, besser, ein anderes Design. Rechne mit ein paar frustrierenden Wochen. Danach wird es viel leichter. HashMaphat keine Reihenfolge.Maps und Objekte in JavaScript behalten die Einfügereihenfolge, und Code verlässt sich oft unbemerkt darauf. EineHashMapin Rust tut das nicht. Sortiere vor der Ausgabe, oder nimmBTreeMapoder das Crateindexmap.- Keine Exceptions. Fehler sind Werte, und du wirst oft
?schreiben..unwrap()aufzurufen ist beim Lernen in Ordnung. Es ist aber die Rust-Version des!aus TypeScript: „Vertrau mir, der Wert ist da.“ Wenn du dich irrst, löst das Programm eine Panic aus. - Kompilierzeiten. Es gibt kein sofortiges Neuladen wie bei
node app.js. Ein erster Build mit vielen Crates dauert eine Weile; spätere Builds sind inkrementell und schneller. Nutze beim Bearbeitencargo check, weil es die Codeerzeugung überspringt.
Ein Lernpfad für JavaScript-Entwickler
Abschnitt betitelt „Ein Lernpfad für JavaScript-Entwickler“Du musst diese Seite nicht der Reihe nach lesen. Als JavaScript- oder TypeScript-Entwickler überspringst du auf diesem Weg, was du schon kannst, und nimmst dir Zeit für das, was in Rust anders ist:
- Erste Schritte mit Rust und Rust-Werkzeuge und Einrichtung: Rust installieren, VS Code mit rust-analyzer einrichten,
cargo newausführen. - Variablen und Datentypen: Lies diese Seiten gründlich. Die Zahlentypen sind neu für dich.
- Ownership, dann Borrowing, dann Slices: die wichtigsten Seiten für dich. Lass dir Zeit.
- Strings:
Stringvs.&str. - Structs und Enums, dann Match und Pattern Matching: deine Interfaces und Discriminated Unions.
- Option und Result, dann Eigene Fehlertypen.
- Closures und Iteratoren: deine Array-Methoden.
- Vektoren, HashMap und HashSet.
- Traits, Generics und Wichtige Traits.
- Module, Cargo und Crates und Testen.
- Lifetimes und Smart Pointer.
- Async/Await und Async im Detail.
- JSON mit Serde, danach baust du das Web-API-Projekt oder probierst WebAssembly aus.
Übungen
Abschnitt betitelt „Übungen“Beide Übungen geben dir ein Stück JavaScript- oder TypeScript-Code. Portiere es nach Rust.
1. Wörter mit einer Map zählen
Abschnitt betitelt „1. Wörter mit einer Map zählen“Dieses JavaScript zählt, wie oft jedes Wort vorkommt, und gibt die Wörter dann vom häufigsten zum seltensten aus (bei Gleichstand alphabetisch). Portiere es nach Rust. Tipp: Nutze eine HashMap<&str, u32> und *counts.entry(word).or_insert(0) += 1. Zum Sortieren sammelst du die Map in einen Vec aus Paaren (word, count) und nutzt 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}`);Lösung anzeigen
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; }
// Wie [...counts]: aus der Map eine Liste von (word, count)-Paaren machen. let mut sorted: Vec<(&str, u32)> = counts.into_iter().collect(); // Häufigste zuerst; bei Gleichstand alphabetisch. sorted.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (word, n) in sorted { println!("{word}: {n}"); }}Ausgabe:
the: 3and: 2bat: 1cat: 1hat: 1Der Beispielsatz bleibt auf Englisch, weil die Ausgabe alphabetisch sortiert wird. entry(word).or_insert(0) erledigt den Teil counts.get(word) ?? 0 und das set in einem Schritt: Es gibt dir eine veränderbare Referenz auf den Zähler und legt ihn mit 0 an, falls er fehlt. Das Sortieren ist hier wirklich nötig, nicht nur nett: Ohne es gibt eine HashMap ihre Einträge bei jedem Lauf in einer anderen Reihenfolge aus.
2. Optionale Einstellungen mit Standardwerten portieren
Abschnitt betitelt „2. Optionale Einstellungen mit Standardwerten portieren“Diese TypeScript-Funktion nimmt ein Options-Objekt, in dem jedes Feld optional ist, und füllt Standardwerte mit ?? auf. Portiere sie nach Rust. Tipp: Leg ein Struct mit Option-Feldern an und füge #[derive(Default)] hinzu. Dann funktioniert ServerOptions { port: Some(3000), ..Default::default() } wie die Übergabe von { port: 3000 }. Für ein bool ist der Standardwert 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 ? "ausführlich" : "still"; return `${host}:${port} (${mode})`;}
console.log(describe({}));console.log(describe({ port: 3000 }));console.log(describe({ host: "example.com", verbose: true }));Lösung anzeigen
#[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 { "ausführlich" } else { "still" }; 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() }) );}Ausgabe:
localhost:8080 (still)localhost:3000 (still)example.com:8080 (ausführlich)unwrap_or ist dein ??. as_deref() macht aus einer Option<String> eine Option<&str>, damit sie auf den einfachen Text "localhost" zurückfallen kann, ohne etwas zu kopieren. Und ..Default::default() ist die Rust-Art zu sagen: „Jedes Feld, das ich nicht erwähnt habe, bekommt seinen Standardwert.“ Genau das passiert in JavaScript, wenn du eine Property weglässt. Ein Port vom Typ u16 bedeutet außerdem, dass ein Port wie 70000 nicht einmal versehentlich gespeichert werden kann.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Vieles aus JavaScript und TypeScript nimmst du mit: Closures, async/await, Destructuring und Array-Methoden wie
map,filterundfindhaben alle nahe Entsprechungen in Rust. - Die großen Umstellungen sind Ownership statt Garbage Collector,
OptionstattundefinedundnullundResult+?stattthrowundtry/catch. - Aus TypeScript-Interfaces werden Structs (für Daten) oder Traits (für Verhalten), und aus Discriminated Unions werden Enums mit
match. - Futures in Rust sind lazy und brauchen eine Runtime wie tokio;
tokio::join!übernimmt die Rolle vonPromise.all. - Cargo ersetzt npm, die
package.jsonund deine Scripts; clippy und rustfmt ersetzen ESLint und Prettier; Tests sind eingebaut. - Statt Express schau dir axum oder actix-web an, statt
fetchreqwest, statt Prisma SeaORM oder Diesel und statt Electron Tauri. Über WebAssembly oder napi-rs kannst du Rust auch in ein JS-Projekt holen.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Was nutzt Rust statt undefined und null?
2. In JavaScript startet der Aufruf einer async-Funktion sie sofort. Was passiert, wenn du in Rust eine async fn ohne .await aufrufst?
3. Dein TypeScript-Code nutzt eine Discriminated Union mit einem Feld kind. Was ist das nächste Feature in Rust?
4. Das Backend deines Teams nutzt Express. Welches Rust-Crate passt am besten?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Die Programmiersprache Rust – Das Rust-Buch: die deutsche Übersetzung des offiziellen Buchs. Für JavaScript-Entwickler sind die Kapitel 4 (Ownership), 6 (Enums und Pattern Matching) und 9 (Fehlerbehandlung) am wichtigsten.
- Tokio Tutorial (auf Englisch): die offizielle Anleitung für async Rust mit tokio, vom ersten Task bis zu Channels und I/O.
- The wasm-bindgen Guide (auf Englisch): wie Rust und JavaScript in WebAssembly miteinander sprechen, mit vielen Beispielen.
- Compiling from Rust to WebAssembly (MDN) (auf Englisch): eine Schritt-für-Schritt-Anleitung aus den MDN Web Docs, inklusive Veröffentlichen auf npm.
- napi-rs (auf Englisch): die Dokumentation, um Node.js-Addons in Rust zu schreiben.