Rust untuk Developer JavaScript dan TypeScript
Kalau kamu menulis JavaScript atau TypeScript, kamu sebenarnya sudah tahu lebih banyak tentang Rust daripada yang kamu kira. Rust punya closure yang bekerja mirip arrow function, async/await, method iterator dengan nama yang akrab seperti map, filter, dan find, destructuring, dan satu tool command-line (Cargo) yang mengerjakan tugas npm. Kalau kamu memakai TypeScript, kamu juga sudah terbiasa dengan compiler yang menangkap kesalahan sebelum kodemu berjalan. Compiler Rust melakukan hal yang sama, hanya lebih ketat.
Jadi, apa yang benar-benar baru? Ada tiga hal. Rust tidak punya garbage collector: setiap nilai punya satu pemilik, dan compiler mengecek ownership (kepemilikan) dan borrowing (peminjaman) sebelum programmu berjalan. Rust tidak punya undefined atau null: nilai yang mungkin tidak ada bertipe Option. Dan Rust tidak punya exception: fungsi yang bisa gagal mengembalikan Result. Sebagian besar waktu belajarmu akan tersita untuk tiga ide ini.
Kenapa developer JavaScript belajar Rust? Biasanya karena salah satu alasan ini: kamu ingin backend web atau tool command-line yang cepat dan dikirim sebagai satu file, kamu ingin mempercepat bagian yang berat di browser dengan WebAssembly, kamu ingin menulis addon native untuk Node.js, atau kamu penasaran dengan tool yang sudah kamu pakai. SWC, Biome, Oxc, Rolldown, Rspack, Turbopack, Lightning CSS, dan Deno semuanya ditulis dengan Rust.
Halaman ini memetakan apa yang sudah kamu kenal dari JavaScript dan TypeScript ke Rust, supaya waktumu bisa dipakai untuk bagian yang benar-benar baru. Kalau kamu justru mencari perbandingan umum dengan C++, Go, dan Python, lihat Rust vs C++, Go, dan Python.
Konsep bahasa: dari JavaScript dan TypeScript ke Rust
Section titled “Konsep bahasa: dari JavaScript dan TypeScript ke Rust”Pakai tabel ini seperti kamus percakapan. Kolom terakhir berisi link ke pelajaran di situs ini yang menjelaskan sisi Rust-nya.
| JavaScript / TypeScript | Rust | Pelajari lebih lanjut |
|---|---|---|
let dan const |
let (tidak bisa diubah) dan let mut (bisa diubah). Isi objek const di JS masih bisa diubah; nilai let di Rust tidak |
Variabel |
number |
Kamu memilih ukurannya: i32, i64, u32, usize, f64, … Integer dan float adalah tipe yang berbeda |
Tipe data |
bigint |
i128 / u128, atau crate num-bigint untuk ukuran berapa pun |
Tipe data |
string |
String (dimiliki, bisa bertambah panjang) dan &str (tampilan teks yang dipinjam) |
String |
boolean |
bool |
Boolean |
| Template literal | format!("Hi {name}") dan println!("Hi {name}") |
Output |
any, unknown |
Tidak ada any. Pakai generics, enum, atau serde_json::Value untuk JSON yang bentuknya tidak diketahui |
Generik, JSON dengan Serde |
interface atau type yang menggambarkan bentuk objek |
struct |
Struct |
interface yang menggambarkan method |
trait |
Trait |
| Structural typing (bentuk sama = tipe sama) | Nominal typing: dua struct dengan field yang sama tetap tipe yang berbeda | Struct |
| Union type dan discriminated union | enum yang setiap varian-nya bisa membawa datanya sendiri |
Enum |
String literal type seperti "small" atau "large" |
enum dengan varian biasa: enum Size { Small, Large } |
Enum |
undefined, null, property opsional x?: T |
Option<T>: bisa Some(value) atau None |
Option |
?. dan ?? |
? (di dalam fungsi yang mengembalikan Option), .map(...), .and_then(...), .unwrap_or(...) |
Option |
Non-null assertion x! |
.unwrap() atau .expect("alasan"), yang menghentikan program kalau nilainya None |
Panic |
throw, try/catch, Promise yang di-reject |
Result<T, E> + operator ? |
Result |
class MyError extends Error |
Enum error buatanmu sendiri (sering dengan thiserror); anyhow di aplikasi |
Error kustom, anyhow dan thiserror |
class dengan field dan method |
struct untuk datanya + blok impl untuk method-nya |
Struct |
extends (inheritance) |
Tidak ada inheritance. Pakai komposisi (struct di dalam struct) + trait dengan method default | Trait, Design pattern |
class Dog implements Animal |
impl Animal for Dog |
Trait |
| Array berisi class yang berbeda-beda dengan interface yang sama | Vec<Box<dyn Animal>> (sebuah trait object) |
Trait object |
Generics seperti T extends HasId |
Generics dengan trait bound: T: HasId |
Generik |
| Arrow function dan callback | Closure, yang dijelaskan oleh trait Fn, FnMut, FnOnce |
Closure, Fn, FnMut, dan FnOnce |
this |
Parameter self, &self, atau &mut self yang eksplisit. Tidak ada kejutan soal this menunjuk ke mana |
Struct |
Destructuring const { x, y } = point |
Pattern: let Point { x, y } = point; (juga di match dan parameter fungsi) |
Pattern matching |
Spread { ...user, admin: true } |
Struct update syntax: User { admin: true, ..user } |
Struct |
switch, atau rantai if berdasarkan typeof / kind |
match (compiler mengecek bahwa semua kasus sudah kamu tangani) |
Match |
=== |
==, yang membandingkan nilai (lewat trait PartialEq). Tidak pernah ada type coercion |
Trait yang sering dipakai |
for...of |
for item in &items |
Perulangan for |
map, filter, reduce, find, some, every |
Method iterator: map, filter, fold, find, any, all, lalu collect |
Iterator |
Generator (function*) dan Symbol.iterator |
Implementasikan trait Iterator |
Iterator kustom |
Promise<T> |
Sebuah Future dengan Output = T. Berbeda dengan Promise, future tidak melakukan apa-apa sampai di-await |
Async/Await |
async / await |
async fn dan .await, plus runtime seperti tokio untuk menjalankannya |
Async/Await, Async lebih dalam |
Promise.all |
tokio::join!, atau join_all dari crate futures |
Async lebih dalam |
setTimeout |
tokio::time::sleep di kode async, std::thread::sleep di luar itu |
Async/Await |
Web Worker, worker_threads |
Thread sungguhan: std::thread::spawn, atau rayon untuk perulangan paralel |
Konkurensi |
ES module: import, export |
Modul: mod, use, dan pub |
Modul |
JSON.parse / JSON.stringify |
serde_json dengan #[derive(Serialize, Deserialize)] |
JSON dengan Serde |
console.log |
println!, dan dbg! untuk debugging cepat. Pakai {:?} untuk tipe yang men-derive Debug |
Output |
readonly, as const, Object.freeze |
Tidak perlu: semuanya read-only kecuali kamu menulis mut |
Variabel |
| Decorator | Attribute seperti #[derive(Debug)] dan makro |
Makro |
Number("abc") menghasilkan NaN |
"abc".parse::<i32>() mengembalikan Err yang harus kamu tangani |
Konversi tipe, Konversi |
| Garbage collector | Ownership dan borrowing, dicek saat compile | Ownership, Borrowing |
Koleksi
Section titled “Koleksi”| JavaScript | Rust | Pelajari lebih lanjut |
|---|---|---|
Array |
Vec<T> (semua item bertipe sama) |
Vektor |
Array dengan panjang tetap, atau tuple TS [string, number] |
[T; N] untuk panjang tetap, tuple (String, i32) untuk tipe campuran |
Array, Tuple |
Map, atau objek yang dipakai sebagai dictionary (Record<string, T>) |
HashMap<K, V> |
HashMap |
| Map dengan key terurut | BTreeMap<K, V> |
HashMap |
Set |
HashSet<T> (atau BTreeSet<T> untuk urutan terurut) |
HashSet |
Array yang dipakai sebagai antrean (push + shift) |
VecDeque<T> (push_back / pop_front, keduanya cepat) |
VecDeque dan BinaryHeap |
Array yang dipakai sebagai stack (push + pop) |
Vec<T> dengan push / pop |
Vektor |
Uint8Array, Buffer |
Vec<u8>, atau byte slice &[u8] |
Slice |
WeakMap, WeakRef |
Weak<T> (dari Rc atau Arc) |
Weak dan Cow |
Satu hal yang perlu diperhatikan: Map, Set, dan objek biasa di JavaScript mengingat urutan kamu memasukkan isinya. HashMap dan HashSet di Rust tidak. Kalau kamu butuh urutan terurut, pakai BTreeMap; kalau kamu butuh urutan penyisipan, crate indexmap memberimu persis itu.
Tooling: dari npm ke cargo
Section titled “Tooling: dari npm ke cargo”Cargo akan terasa seperti rumah sendiri. Ia adalah package manager, build tool, test runner, dan script runner sekaligus, dan setiap proyek Rust memakainya dengan cara yang sama.
| JavaScript | Rust | Catatan |
|---|---|---|
node + npm (atau pnpm, yarn) |
cargo |
Mem-build, menjalankan, mengetes, menambah package, mem-publish. Lihat Cargo dan crate |
package.json |
Cargo.toml |
|
package-lock.json |
Cargo.lock |
Dibuat otomatis untukmu |
node_modules/ |
Tidak ada folder per proyek. Unduhan di-cache sekali di ~/.cargo, hasil build masuk ke target/ |
|
| npmjs.com | crates.io | Dokumentasi API setiap crate yang dipublikasikan ada di docs.rs |
npm install zod |
cargo add serde |
|
npm init |
cargo new my_app (cargo new --lib untuk library) |
|
npm run dev / build / test |
cargo run / cargo build / cargo test |
Tambahkan --release untuk build yang dioptimalkan; build debug jauh lebih lambat |
"scripts" di package.json |
Tidak ada bagian scripts. Pakai alias cargo di .cargo/config.toml, atau task runner seperti just |
|
npx some-tool |
cargo install some-tool, lalu jalankan |
cargo binstall mengunduh binary yang sudah jadi alih-alih meng-compile-nya |
nvm dan .nvmrc |
rustup dan file rust-toolchain.toml |
Lihat Tools dan setup Rust |
| Workspace npm / pnpm (monorepo) | Cargo workspace | Satu Cargo.toml di level teratas mendaftar crate-crate anggotanya |
tsc |
rustc, dipanggil untukmu oleh cargo |
|
tsc --noEmit |
cargo check |
Mengecek tipe tanpa mem-build; jauh lebih cepat |
| ESLint | cargo clippy |
Ratusan lint yang membantu, resmi |
| Prettier | cargo fmt |
Satu gaya standar untuk semua orang |
nodemon, tsx watch |
cargo watch atau bacon |
Diinstal dengan cargo install |
| Jest / Vitest / Mocha | #[test] bawaan dan cargo test (cargo nextest adalah runner populer yang lebih cepat) |
Lihat Testing |
| Snapshot Jest | insta |
Lihat Pengujian lanjutan |
jest.mock() |
mockall, atau fake buatan sendiri yang mengimplementasikan trait-mu |
|
| fast-check | proptest |
Property-based testing |
| c8 / Istanbul (coverage) | cargo llvm-cov |
|
| JSDoc / TypeDoc | Doc comment /// dalam Markdown + cargo doc |
Contoh kode di dokumentasi dijalankan sebagai test. Lihat Dokumentasi |
| VS Code + TypeScript server | VS Code + rust-analyzer, atau JetBrains RustRover | rust-analyzer memberi completion, go-to-definition, dan error langsung di baris kode |
npm publish |
cargo publish |
Ini contoh Cargo.toml kecil, supaya kamu bisa membandingkannya dengan 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"] }Versi seperti "1" artinya sama dengan "^1" di package.json: rilis 1.x apa pun yang kompatibel. Feature adalah bagian opsional dari sebuah crate yang bisa kamu nyalakan; lihat Fitur Cargo dan cfg.
Library dan framework: “Kalau begitu, pakai apa sebagai gantinya…?”
Section titled “Library dan framework: “Kalau begitu, pakai apa sebagai gantinya…?””Inilah pertanyaan pertama setiap developer JavaScript. “Kami pakai Express dan Prisma. Di Rust ada apa?” Berikut padanan yang paling dekat. Seperti npm, crates.io punya banyak pilihan untuk hampir setiap kebutuhan; yang di bawah ini adalah yang populer dan dirawat dengan baik.
Web dan networking
Section titled “Web dan networking”| JavaScript | Rust | Catatan |
|---|---|---|
| Express, Fastify, Koa | axum (kita membuatnya di Proyek: Web API dengan Axum), actix-web, Rocket | axum dan actix-web banyak dipakai di production |
| Middleware Express | Layer tower dan crate tower-http (CORS, kompresi, tracing, timeout) |
axum memakai tower secara langsung |
fetch, axios |
reqwest |
Async secara default, dengan client blocking opsional. Lihat Jaringan di Rust |
ws, Socket.IO |
WebSocket di axum (fitur ws), tokio-tungstenite, atau socketioxide (server Socket.IO) |
|
| Apollo Server (GraphQL) | async-graphql |
|
gRPC (@grpc/grpc-js) |
tonic (dengan prost untuk protobuf) |
|
| Generator Swagger / OpenAPI | utoipa |
|
jsonwebtoken |
jsonwebtoken |
Nama sama, ide sama |
bcrypt |
argon2 atau bcrypt |
Akses data
Section titled “Akses data”| JavaScript | Rust | Catatan |
|---|---|---|
| Prisma, TypeORM | SeaORM (async, entity, migrasi) atau Diesel (query dicek tipenya saat compile) | SeaORM paling terasa mirip Prisma dan TypeORM |
Knex, pg, postgres.js |
sqlx: kamu menulis SQL biasa, dan makro query! mengeceknya terhadap database sungguhan saat compile |
Async, sangat populer, bukan ORM |
better-sqlite3 |
rusqlite, atau sqlx dengan SQLite |
|
| Mongoose, driver MongoDB | mongodb (driver resmi) |
Tidak ada skema ala Mongoose; kamu memakai struct serde |
| ioredis | redis |
Database di Rust menunjukkan sqlx, Diesel, dan SeaORM dengan kode yang benar-benar jalan.
Komponen sehari-hari
Section titled “Komponen sehari-hari”| JavaScript | Rust | Catatan |
|---|---|---|
JSON.parse / JSON.stringify |
serde + serde_json |
Standarnya; juga bisa TOML, YAML, dan lainnya. Lihat JSON dengan Serde |
| Zod, Yup | Tipe-tipe di struct-mu + serde mengerjakan sebagian besar tugasnya; validator atau garde untuk aturan seperti “minimal 8 karakter” |
Kalau JSON-nya tidak cocok dengan struct-nya, parsing gagal dengan error |
| Lodash | Sebagian besar sudah ada di iterator standard library; itertools untuk tambahan seperti unique, sorted_by, dan chunk_by |
|
| date-fns, Day.js, Luxon | chrono atau jiff |
Lihat Tanggal dan waktu |
| dotenv | dotenvy |
Lihat Environment dan proses |
| Commander, yargs | clap |
Lihat Proyek: CLI dengan clap |
| chalk | colored atau owo-colors |
|
| winston, pino | tracing (+ tracing-subscriber), atau log + env_logger |
Lihat Logging |
uuid |
uuid |
|
EventEmitter |
Channel (std::sync::mpsc, atau tokio::sync::broadcast untuk banyak pendengar) |
Lihat Channel |
| BullMQ (antrean job) | tokio::spawn untuk task di dalam proses; apalis untuk antrean job yang persisten |
|
| Regular expression | regex |
Tidak ada lookahead atau backreference, memang disengaja. Lihat Regular expression |
Frontend, desktop, dan Node.js
Section titled “Frontend, desktop, dan Node.js”| JavaScript | Rust | Catatan |
|---|---|---|
| Mempercepat kode browser yang berat | Compile Rust ke WebAssembly dengan wasm-bindgen dan wasm-pack |
Lihat Rust dan WebAssembly dan bagian di bawah |
| React, Vue, Svelte | Leptos, Yew, Dioxus (semuanya di-compile ke WebAssembly) |
Semuanya belum versi 1.0; API-nya masih berubah |
| Electron | Tauri: frontend web-mu + backend Rust, dengan aplikasi yang jauh lebih kecil |
Tauri 2 sudah stabil. Lihat Aplikasi GUI |
| Addon native Node.js (C++ dengan node-gyp) | napi + napi-derive (napi-rs), atau neon |
Kirim binary yang sudah jadi ke npm; tidak perlu C++ |
| Game Phaser, Three.js | Bevy, macroquad |
Bevy belum 1.0 dan API-nya berubah di setiap rilis. Lihat Pengembangan game |
Berdampingan: interface menjadi struct
Section titled “Berdampingan: interface menjadi struct”Ini objek TypeScript dengan interface, fungsi yang mirip method, dan salinan dengan 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);Di Rust, bentuknya menjadi sebuah struct, dan fungsinya menjadi method di blok impl:
#[derive(Debug, Clone)]struct User { name: String, age: u32, admin: bool,}
impl User { // `&self` artinya "method ini membaca user, tidak mengubahnya". fn greeting(&self) -> String { format!("Halo, aku {} ({})", self.name, self.age) }}
fn main() { let ada = User { name: "Ada".to_string(), age: 36, admin: false };
// Seperti { ...ada, admin: true } di JavaScript. let promoted = User { admin: true, ..ada.clone() };
println!("{}", ada.greeting()); println!("{:?}", promoted);}Output:
Halo, aku Ada (36)User { name: "Ada", age: 36, admin: true }Ada beberapa hal yang berbeda. Setiap field punya tipe yang pasti: age bertipe u32 (bilangan bulat yang tidak bisa negatif), bukan number yang umum. #[derive(Debug)] itulah yang membuatmu bisa mencetak seluruh struct dengan {:?}, seperti console.log(obj). Lalu kenapa ada .clone()? Spread ..ada akan memindahkan string name keluar dari ada, dan setelah itu kita tidak bisa memakai ada lagi. Meng-clone dulu menjaga ada tetap utuh. Itulah ownership yang sedang bekerja, dan kamu akan sering melihatnya.
Berdampingan: discriminated union menjadi enum
Section titled “Berdampingan: discriminated union menjadi enum”Developer TypeScript suka discriminated union: field kind memberitahumu bentuk mana yang sedang kamu pegang, dan switch mempersempit tipenya:
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 punya fitur ini secara bawaan, dan ini salah satu fitur terbaiknya. Setiap varian enum membawa datanya sendiri, dan match membongkarnya:
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.00Tidak perlu string kind: varian itu sendiri sudah menjadi penandanya. Dan match harus mencakup semua varian. Kalau nanti kamu menambahkan Triangle, setiap match yang melupakannya menjadi error compile. Ini mirip trik exhaustiveness dengan never di TypeScript, tapi selalu aktif. Selengkapnya di Pattern matching.
Berdampingan: undefined dan null menjadi Option
Section titled “Berdampingan: undefined dan null menjadi Option”Di TypeScript, nilai opsional muncul sebagai undefined, null, atau property opsional, dan kamu memakai ?. dan ?? untuk menanganinya:
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"}`);}Di Rust, nilai yang mungkin tidak ada bertipe Option<T>. Isinya bisa Some(value) atau None, dan kamu tidak bisa memakainya sebagai nilai biasa sebelum menangani kasus 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, }}
// Di dalam fungsi yang mengembalikan Option, `?` bekerja seperti `?.`:// kalau nilainya None, berhenti di sini dan kembalikan 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] { // Seperti findUser(id)?.email?.toUpperCase() ?? "tidak ada email" let email = find_user(id) .and_then(|user| user.email) .map(|email| email.to_uppercase()) .unwrap_or("tidak ada email".to_string()); println!("{id}: {email}"); }
println!("{:?}", email_domain(1)); println!("{:?}", email_domain(2));
if let Some(user) = find_user(2) { println!("ketemu {}", user.name); }}Output:
2: tidak ada email3: tidak ada emailSome("example.com")Noneketemu GraceTidak ada “cannot read properties of undefined” di Rust, karena tidak ada undefined yang bisa dibaca. Compiler tidak akan membiarkanmu memanggil .to_uppercase() pada Option<String>. Kamu juga tidak perlu mengingat perbedaan null dan undefined: yang ada hanya None. Pelajari method-method pembantunya di halaman Option, dan bentuk-bentuk singkatnya di If Let / While Let.
Berdampingan: try/catch menjadi Result dan ?
Section titled “Berdampingan: try/catch menjadi Result dan ?”Di JavaScript, sebuah fungsi melempar error, dan kamu menangkapnya di suatu tempat di atas call stack. Tidak ada apa pun di signature fungsinya yang memberitahumu bahwa ia bisa melempar error:
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}`); }}Di Rust, fungsi yang bisa gagal mengembalikan Result: bisa Ok(value) atau Err(error). Operator ? berarti “kalau ini gagal, kembalikan error-nya ke pemanggilku sekarang juga”. Fungsinya sama seperti membiarkan exception naik ke atas, tapi kamu bisa melihatnya di kode:
fn parse_age(input: &str) -> Result<u32, String> { let age: u32 = input .parse() .map_err(|_| format!("'{input}' bukan bilangan bulat"))?;
if age > 150 { return Err(format!("{age} terlalu tua")); } Ok(age)}
fn total_age(a: &str, b: &str) -> Result<u32, String> { // Setiap `?` meneruskan error ke pemanggil kita, seperti 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!("Error: {e}"), } }}Output:
Total: 77Error: 'abc' bukan bilangan bulatError: 200 terlalu tuaSignature -> Result<u32, String> memberi tahu setiap pemanggil bahwa “ini bisa gagal”, dan compiler tidak akan membiarkan mereka mengabaikannya. Hal yang sama berlaku untuk kode async: di tempat JavaScript memberimu Promise yang di-reject, fungsi async di Rust mengembalikan Result, dan kamu menulis .await?. Untuk tipe error buatanmu sendiri, lihat Error kustom. Di aplikasi, crate anyhow memberimu sesuatu yang mirip “tangkap error apa saja” (lihat anyhow dan thiserror).
Berdampingan: method array menjadi iterator
Section titled “Berdampingan: method array menjadi iterator”Method array yang kamu pakai setiap hari punya padanan langsung di 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)}`);Versi Rust-nya memakai iterator dan closure. Sintaks closure-nya |o| o.paid, bukan (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, lalu collect() mengubah hasilnya menjadi Vec. let paid_names: Vec<&str> = orders .iter() .filter(|o| o.paid) .map(|o| o.customer.as_str()) .collect();
// reduce dengan nilai awal disebut fold. let revenue = orders .iter() .filter(|o| o.paid) .fold(0, |sum, o| sum + o.total);
// find mengembalikan Option, karena mungkin tidak ada yang cocok. let first_big = orders.iter().find(|o| o.total > 150);
println!("{}", paid_names.join(", ")); println!("Pendapatan: {revenue}"); match first_big { Some(o) => println!("Pesanan besar pertama: {}", o.customer), None => println!("Pesanan besar pertama: tidak ada"), } // some() di sini namanya any(). println!("Ada yang belum dibayar? {}", orders.iter().any(|o| !o.paid));}Output:
Ada, LinusPendapatan: 320Pesanan besar pertama: LinusAda yang belum dibayar? trueAda satu perbedaan besar dengan JavaScript. Setiap method array di JS langsung membuat array baru yang utuh. Iterator Rust bersifat lazy: filter dan map tidak melakukan apa-apa sendirian, dan pekerjaannya terjadi dalam satu kali jalan saat kamu memanggil sesuatu seperti collect(), sum(), fold(), atau perulangan for. Tidak ada array sementara yang dibuat di tengah jalan. Untuk menjumlahkan saja, kamu juga bisa menulis .map(|o| o.total).sum::<u32>(). Pengurutan ada di Vec (sort, sort_by), bukan di iterator; lihat Mengurutkan dan mencari.
Berdampingan: Promise menjadi future + tokio
Section titled “Berdampingan: Promise menjadi future + tokio”Ini sedikit JavaScript async. Perhatikan kapan "fetching 1" dicetak:
async function fetchUser(id) { console.log(`fetching ${id}`); await new Promise((resolve) => setTimeout(resolve, 100)); return `user ${id}`;}
const promise = fetchUser(1); // langsung mencetak "fetching 1"console.log("promise created");console.log(await promise);
const [a, b] = await Promise.all([fetchUser(2), fetchUser(3)]);console.log(`${a}, ${b}`);Di JavaScript, memanggil fungsi async langsung menjalankannya. Rust berbeda: memanggil async fn hanya membuat sebuah future, yaitu deskripsi pekerjaan. Tidak ada yang berjalan sampai kamu meng-.await-nya. Rust juga tidak punya event loop bawaan, jadi kamu menambahkan runtime: tokio adalah yang paling populer.
use std::time::Duration;
async fn fetch_user(id: u32) -> String { println!("mengambil {id}"); tokio::time::sleep(Duration::from_millis(100)).await; format!("user {id}")}
#[tokio::main]async fn main() { // Membuat future TIDAK menjalankannya. Belum ada yang dicetak. let future = fetch_user(1); println!("future dibuat"); println!("{}", future.await);
// Seperti Promise.all: jalankan keduanya bersamaan dan tunggu keduanya. let (a, b) = tokio::join!(fetch_user(2), fetch_user(3)); println!("{a}, {b}");}Output:
future dibuatmengambil 1user 1mengambil 2mengambil 3user 2, user 3Lihat urutannya? Di JavaScript, “fetching 1” muncul sebelum “promise created”. Di Rust, “mengambil 1” muncul setelah “future dibuat”, karena future baru mulai berjalan saat .await. #[tokio::main] menyiapkan runtime (event loop-mu) dan menjalankan main di atasnya. tokio::join! menunggu kedua future bersamaan, seperti Promise.all; tokio::spawn menjalankan sebuah task di latar belakang, yang paling mirip dengan memanggil fungsi async tanpa meng-await-nya. Pelajaran Async/Await dan Async lebih dalam menjelaskan detailnya.
Rust di proyek JavaScript-mu: WebAssembly dan addon Node
Section titled “Rust di proyek JavaScript-mu: WebAssembly dan addon Node”Kamu tidak perlu menulis ulang apa pun untuk mulai memakai Rust. Ada dua cara umum untuk memasukkannya ke proyek JavaScript:
WebAssembly di browser. Tandai fungsi Rust dengan #[wasm_bindgen], build dengan wasm-pack, lalu import hasilnya seperti ES module biasa. wasm-pack bahkan menuliskan file .d.ts, jadi TypeScript tahu tipe-tipe fungsimu:
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(); // unduh dan jalankan file .wasmconsole.log(slugify("Hello Rust World")); // "hello-rust-world"Pelajaran Rust dan WebAssembly memandumu melewati seluruh setup-nya, dan menjelaskan nilai mana yang murah untuk dilewatkan antara JS dan Wasm. Satu tips: Wasm sepadan untuk komputasi berat (pengolahan gambar, parsing, simulasi), bukan untuk fungsi kecil yang kamu panggil ribuan kali, karena setiap pemanggilan antara JS dan Wasm punya sedikit biaya.
Addon native Node.js. Dengan napi-rs (crate napi dan napi-derive), kamu menandai fungsi Rust dengan #[napi], dan fungsi itu bisa kamu require atau import dari Node. Banyak package npm populer mengirim binary Rust yang sudah jadi dengan cara ini, jadi penggunanya tidak pernah butuh compiler Rust.
Hal-hal yang akan membuatmu tersandung sebagai developer JavaScript
Section titled “Hal-hal yang akan membuatmu tersandung sebagai developer JavaScript”Semua orang yang datang dari JavaScript menabrak polisi tidur yang sama. Mengetahuinya lebih awal akan membantu.
Memberikan nilai ke fungsi berarti memindahkannya. Di JavaScript, memberikan array ke sebuah fungsi berarti memberikan referensi, dan kamu masih bisa memakai array itu setelahnya. Di Rust, memberikan Vec atau String secara by value memindahkan (move) ownership ke dalam fungsi, dan variabel aslinya tidak bisa dipakai lagi:
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 moveCompiler memberitahumu bahwa names sudah dipindahkan ke print_all di baris 9 (“value moved here”), lalu kamu mencoba memakainya lagi di baris 10. Solusi yang biasa adalah meminjam: buat fungsinya menerima names: &[String] dan panggil dengan print_all(&names). Baca Ownership dan Borrowing dulu; kedua halaman itu menjelaskan aturan ini, dan kamu akan bertemu dengannya setiap hari.
Angka punya tipe, dan tipenya tidak bisa dicampur. JavaScript punya satu number. Di Rust, 3 adalah integer dan 2.5 adalah float, dan kamu tidak bisa menggabungkannya tanpa konversi:
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}`Pesan ini artinya: tidak ada cara bawaan untuk mengalikan integer dengan float. Tulis items as f64 * price, atau jadikan items float sejak awal (3.0). Perhatikan juga bahwa pembagian integer membuang sisanya: 7 / 2 hasilnya 3, bukan 3.5. Lihat Tipe data dan Konversi tipe.
Beberapa hal lain yang akan kamu temui:
- Future bersifat lazy dan butuh runtime. Seperti yang kamu lihat di atas, tidak ada yang terjadi sampai
.await, dan kamu butuh tokio (atau runtime lain) untuk bisa menjalankan kode async sama sekali. Lupa menulis.awaithanya menghasilkan warning dari compiler, bukan task yang berjalan. - String tidak bisa diakses dengan indeks.
name[0]tidak bisa di-compile, karenaStringdi Rust berformat UTF-8 dan satu karakter bisa memakan beberapa byte. Pakainame.chars().next()atau.chars().nth(i). Pelajari juga perbedaanStringdan&strsejak awal: fungsi biasanya menerima&str, struct biasanya menyimpanString. Lihat String. - Tidak ada structural typing dan tidak ada
any. Di TypeScript, objek apa pun dengan field yang tepat cocok dengan sebuah interface. Di Rust, fungsi yang menerimaUserhanya menerimaUser. Untuk perilaku bersama, pakai trait. Untuk JSON yang bentuknya tidak kamu ketahui, pakaiserde_json::Value. - Graf objek yang dibagi bersama dan bisa diubah itu sulit. Di JavaScript, wajar kalau satu objek dioper ke mana-mana dan semua orang boleh mengubahnya, atau objek menunjuk balik ke parent-nya. Borrow checker mengizinkan banyak pembaca atau satu penulis pada satu waktu, tidak pernah keduanya. Pola seperti itu butuh smart pointer (
Rc<RefCell<T>>,Arc<Mutex<T>>) atau, lebih baik lagi, desain yang berbeda. Bersiaplah untuk beberapa minggu yang bikin frustrasi. Setelahnya jauh lebih mudah. HashMaptidak punya urutan.Mapdan objek di JavaScript menjaga urutan penyisipan, jadi kode sering bergantung padanya tanpa disadari.HashMapdi Rust tidak. Urutkan dulu sebelum mencetak, atau pakaiBTreeMapatau crateindexmap.- Tidak ada exception. Error adalah nilai, dan kamu akan sering menulis
?. Memanggil.unwrap()tidak apa-apa selama belajar, tapi itu versi Rust dari!di TypeScript: “percaya saja, nilainya ada”. Kalau ternyata salah, programnya panic. - Waktu compile. Tidak ada reload instan seperti
node app.js. Build pertama dengan banyak crate butuh waktu; build berikutnya incremental dan lebih cepat. Pakaicargo checkselagi kamu mengedit, karena ia melewati tahap pembuatan kode.
Jalur belajar untuk developer JavaScript
Section titled “Jalur belajar untuk developer JavaScript”Kamu tidak perlu membaca situs ini berurutan. Sebagai developer JavaScript atau TypeScript, rute ini melewati apa yang sudah kamu tahu dan memberi waktu lebih untuk bagian Rust yang berbeda:
- Mulai dengan Rust dan Tools dan setup Rust: instal Rust, siapkan VS Code dengan rust-analyzer, jalankan
cargo new. - Variabel dan Tipe data: baca dengan saksama. Tipe-tipe angkanya masih baru untukmu.
- Ownership, lalu Borrowing, lalu Slice: halaman paling penting untukmu. Jangan terburu-buru.
- String:
Stringvs&str. - Struct dan Enum, lalu Match dan Pattern matching: interface dan discriminated union versimu.
- Option dan Result, lalu Error kustom.
- Closure dan Iterator: method array versimu.
- Vektor, HashMap, dan HashSet.
- Trait, Generik, dan Trait yang sering dipakai.
- Modul, Cargo dan crate, dan Testing.
- Lifetime dan Smart pointer.
- Async/Await dan Async lebih dalam.
- JSON dengan Serde, lalu bangun proyek Web API, atau coba WebAssembly.
Latihan
Section titled “Latihan”Kedua latihan memberimu potongan kode JavaScript atau TypeScript. Porting kode itu ke Rust.
1. Menghitung kata dengan Map
Section titled “1. Menghitung kata dengan Map”Kode JavaScript ini menghitung berapa kali setiap kata muncul, lalu mencetak kata-katanya dari yang paling sering sampai yang paling jarang (kalau seri, urut abjad). Porting kode ini ke Rust. Petunjuk: pakai HashMap<&str, u32> dan *counts.entry(word).or_insert(0) += 1. Untuk mengurutkan, kumpulkan isi map ke dalam Vec berisi pasangan (word, count) lalu pakai 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}`);Lihat solusi
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; }
// Seperti [...counts]: ubah map menjadi list pasangan (kata, jumlah). let mut sorted: Vec<(&str, u32)> = counts.into_iter().collect(); // Yang paling sering duluan; kalau seri, urut abjad. 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) mengerjakan bagian counts.get(word) ?? 0 dan set sekaligus: ia memberimu referensi mutable ke jumlahnya, dan membuatnya dengan nilai 0 kalau belum ada. Pengurutan di sini wajib, bukan sekadar bagus: tanpanya, HashMap mencetak dalam urutan yang berbeda dari satu kali jalan ke kali berikutnya. Kalimatnya sengaja dibiarkan dalam bahasa Inggris, karena urutan abjad kata-katanya ikut menentukan output.
2. Porting pengaturan opsional dengan nilai default
Section titled “2. Porting pengaturan opsional dengan nilai default”Fungsi TypeScript ini menerima objek opsi yang semua field-nya opsional, lalu mengisi nilai default dengan ??. Porting kode ini ke Rust. Petunjuk: buat struct dengan field Option dan tambahkan #[derive(Default)]. Lalu ServerOptions { port: Some(3000), ..Default::default() } bekerja seperti memberikan { port: 3000 }. Untuk bool, nilai default-nya 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 }));Lihat solusi
#[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 adalah ?? versimu. as_deref() mengubah Option<String> menjadi Option<&str>, sehingga ia bisa jatuh ke teks biasa "localhost" tanpa menyalin apa pun. Dan ..Default::default() adalah cara Rust untuk berkata “setiap field yang tidak kusebut mendapat nilai default-nya”, persis seperti yang terjadi saat kamu tidak menulis sebuah property di JavaScript. Port bertipe u16 juga berarti port seperti 70000 bahkan tidak bisa tersimpan karena salah ketik.
Ringkasan
Section titled “Ringkasan”- Banyak hal dari JavaScript dan TypeScript tetap terpakai: closure, async/await, destructuring, dan method ala array seperti
map,filter, danfindsemuanya punya padanan yang dekat di Rust. - Perubahan besarnya: ownership menggantikan garbage collector,
Optionmenggantikanundefineddannull, danResult+?menggantikanthrowdantry/catch. - Interface TypeScript menjadi struct (untuk data) atau trait (untuk perilaku), dan discriminated union menjadi enum dengan
match. - Future di Rust bersifat lazy dan butuh runtime seperti tokio;
tokio::join!berperan sebagaiPromise.all. - Cargo menggantikan npm,
package.json, dan script-script-mu; clippy dan rustfmt menggantikan ESLint dan Prettier; testing sudah bawaan. - Sebagai pengganti Express, lihat axum atau actix-web; untuk
fetch, lihat reqwest; untuk Prisma, lihat SeaORM atau Diesel; dan untuk Electron, lihat Tauri. Kamu juga bisa membawa Rust ke proyek JS lewat WebAssembly atau napi-rs.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Apa yang dipakai Rust sebagai pengganti undefined dan null?
2. Di JavaScript, memanggil fungsi async langsung menjalankannya. Apa yang terjadi kalau kamu memanggil async fn di Rust tanpa .await?
3. Kode TypeScript-mu memakai discriminated union dengan field kind. Fitur Rust apa yang paling dekat?
4. Backend timmu memakai Express. Crate Rust mana yang paling mirip?
Skor: 0 / 4
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- The Rust Programming Language (bahasa Inggris): buku resmi Rust. Bab 4 (ownership), 6 (enum dan pattern matching), dan 9 (error) yang paling penting untuk developer JavaScript.
- Tokio tutorial (bahasa Inggris): panduan resmi Rust async dengan tokio, dari task pertamamu sampai channel dan I/O.
- The wasm-bindgen Guide (bahasa Inggris): cara Rust dan JavaScript berkomunikasi di WebAssembly, dengan banyak contoh.
- Compiling from Rust to WebAssembly (MDN) (bahasa Inggris): tutorial langkah demi langkah dari MDN Web Docs, termasuk cara mem-publish ke npm.
- napi-rs (bahasa Inggris): dokumentasi untuk menulis addon Node.js dengan Rust.