Lewati ke konten

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
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.

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.

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
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.

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
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

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 }
▶ Coba sendiri

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.14
6.00
▶ Coba sendiri

Tidak 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 === 1) return { name: "Ada", email: "[email protected]" };
if (id === 2) return { name: "Grace" };
return undefined;
}
for (const id of [1, 2, 3]) {
console.log(`${id}: ${findUser(id)?.email?.toUpperCase() ?? "no email"}`);
}

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 {
1 => Some(User { name: "Ada".to_string(), email: Some("[email protected]".to_string()) }),
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 email
3: tidak ada email
Some("example.com")
None
ketemu Grace
▶ Coba sendiri

Tidak 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: 77
Error: 'abc' bukan bilangan bulat
Error: 200 terlalu tua
▶ Coba sendiri

Signature -> 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, Linus
Pendapatan: 320
Pesanan besar pertama: Linus
Ada yang belum dibayar? true
▶ Coba sendiri

Ada 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 dibuat
mengambil 1
user 1
mengambil 2
mengambil 3
user 2, user 3
▶ Coba sendiri

Lihat 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 .wasm
console.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 here
10 | println!("{} names", names.len());
| ^^^^^ value borrowed here after move

Compiler 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 .await hanya menghasilkan warning dari compiler, bukan task yang berjalan.
  • String tidak bisa diakses dengan indeks. name[0] tidak bisa di-compile, karena String di Rust berformat UTF-8 dan satu karakter bisa memakan beberapa byte. Pakai name.chars().next() atau .chars().nth(i). Pelajari juga perbedaan String dan &str sejak awal: fungsi biasanya menerima &str, struct biasanya menyimpan String. 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 menerima User hanya menerima User. Untuk perilaku bersama, pakai trait. Untuk JSON yang bentuknya tidak kamu ketahui, pakai serde_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.
  • HashMap tidak punya urutan. Map dan objek di JavaScript menjaga urutan penyisipan, jadi kode sering bergantung padanya tanpa disadari. HashMap di Rust tidak. Urutkan dulu sebelum mencetak, atau pakai BTreeMap atau crate indexmap.
  • 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. Pakai cargo check selagi kamu mengedit, karena ia melewati tahap pembuatan kode.

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:

  1. Mulai dengan Rust dan Tools dan setup Rust: instal Rust, siapkan VS Code dengan rust-analyzer, jalankan cargo new.
  2. Variabel dan Tipe data: baca dengan saksama. Tipe-tipe angkanya masih baru untukmu.
  3. Ownership, lalu Borrowing, lalu Slice: halaman paling penting untukmu. Jangan terburu-buru.
  4. String: String vs &str.
  5. Struct dan Enum, lalu Match dan Pattern matching: interface dan discriminated union versimu.
  6. Option dan Result, lalu Error kustom.
  7. Closure dan Iterator: method array versimu.
  8. Vektor, HashMap, dan HashSet.
  9. Trait, Generik, dan Trait yang sering dipakai.
  10. Modul, Cargo dan crate, dan Testing.
  11. Lifetime dan Smart pointer.
  12. Async/Await dan Async lebih dalam.
  13. JSON dengan Serde, lalu bangun proyek Web API, atau coba WebAssembly.

Kedua latihan memberimu potongan kode JavaScript atau TypeScript. Porting kode itu ke Rust.

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: 3
and: 2
bat: 1
cat: 1
hat: 1
▶ Coba sendiri

entry(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)
▶ Coba sendiri

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.

  • Banyak hal dari JavaScript dan TypeScript tetap terpakai: closure, async/await, destructuring, dan method ala array seperti map, filter, dan find semuanya punya padanan yang dekat di Rust.
  • Perubahan besarnya: ownership menggantikan garbage collector, Option menggantikan undefined dan null, dan Result + ? menggantikan throw dan try/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 sebagai Promise.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

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

  • 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.