Rust para desenvolvedores JavaScript e TypeScript
Se você escreve JavaScript ou TypeScript, já sabe mais Rust do que imagina. O Rust tem closures que funcionam de um jeito bem parecido com as arrow functions, async/await, métodos de iteradores com nomes conhecidos como map, filter e find, desestruturação, e uma ferramenta de linha de comando (Cargo) que faz o que o npm faz. Se você usa TypeScript, já está acostumado com um compilador que pega erros antes de o código rodar. O compilador do Rust faz a mesma coisa, só que de forma mais rigorosa.
Então, o que é novo de verdade? Três coisas. O Rust não tem garbage collector (coletor de lixo): cada valor tem um único dono, e o compilador verifica o ownership (posse) e o borrowing (empréstimo) antes de o programa rodar. O Rust não tem undefined nem null: um valor que pode estar faltando tem o tipo Option. E o Rust não tem exceções: uma função que pode falhar devolve um Result. A maior parte do seu tempo de estudo vai para essas três ideias.
Por que desenvolvedores JavaScript aprendem Rust? Normalmente por um destes motivos: você quer um backend web ou uma ferramenta de linha de comando rápida, entregue como um único arquivo; quer acelerar um trecho pesado no navegador com WebAssembly; quer escrever um addon nativo para o Node.js; ou tem curiosidade sobre as ferramentas que já usa. SWC, Biome, Oxc, Rolldown, Rspack, Turbopack, Lightning CSS e Deno são todos escritos em Rust.
Esta página mostra o equivalente em Rust do que você já conhece do JavaScript e do TypeScript, para você gastar o seu tempo nas partes que são realmente novas. Se você prefere uma comparação geral com C++, Go e Python, veja Rust vs outras linguagens.
Conceitos da linguagem: do JavaScript e TypeScript para o Rust
Seção intitulada “Conceitos da linguagem: do JavaScript e TypeScript para o Rust”Use esta tabela como um guia de conversação. A última coluna leva à lição deste site que explica o lado do Rust.
| JavaScript / TypeScript | Rust | Saiba mais |
|---|---|---|
let e const |
let (não muda) e let mut (pode mudar). Um objeto const do JS ainda pode ser alterado por dentro; um valor let do Rust não |
Variáveis |
number |
Você escolhe um tamanho: i32, i64, u32, usize, f64, … Inteiros e floats são tipos diferentes |
Tipos de dados |
bigint |
i128 / u128, ou o crate num-bigint para qualquer tamanho |
Tipos de dados |
string |
String (com dono, pode crescer) e &str (uma visão emprestada de um texto) |
Strings |
boolean |
bool |
Booleanos |
| Template literals | format!("Oi {name}") e println!("Oi {name}") |
Saída em Rust |
any, unknown |
Não existe any. Use genéricos, um enum, ou serde_json::Value para um JSON de formato desconhecido |
Genéricos, JSON com Serde |
interface ou type que descreve o formato de um objeto |
struct |
Structs |
interface que descreve métodos |
trait |
Traits |
| Tipagem estrutural (mesmo formato = mesmo tipo) | Tipagem nominal: duas structs com os mesmos campos continuam sendo tipos diferentes | Structs |
| Union types e discriminated unions | enum em que cada variante pode carregar os próprios dados |
Enums |
Tipos literais de string como "small" ou "large" |
Um enum com variantes simples: enum Size { Small, Large } |
Enums |
undefined, null, propriedade opcional x?: T |
Option<T>: ou Some(valor) ou None |
Option |
?. e ?? |
? (dentro de uma função que devolve Option), .map(...), .and_then(...), .unwrap_or(...) |
Option |
Non-null assertion x! |
.unwrap() ou .expect("motivo"), que encerram o programa se o valor for None |
Panic |
throw, try/catch, Promises rejeitadas |
Result<T, E> + o operador ? |
Result |
class MyError extends Error |
Um enum de erro seu (muitas vezes com thiserror); anyhow em aplicações |
Erros personalizados, anyhow e thiserror |
class com campos e métodos |
struct para os dados + um bloco impl para os métodos |
Structs |
extends (herança) |
Não existe herança. Use composição (uma struct dentro de outra) + traits com métodos padrão | Traits, Padrões de projeto |
class Dog implements Animal |
impl Animal for Dog |
Traits |
| Um array de classes diferentes que compartilham uma interface | Vec<Box<dyn Animal>> (um trait object) |
Trait objects |
Genéricos como T extends HasId |
Genéricos com trait bounds: T: HasId |
Genéricos |
| Arrow functions e callbacks | Closures, descritas pelos traits Fn, FnMut, FnOnce |
Closures, Fn, FnMut e FnOnce |
this |
Um parâmetro explícito self, &self ou &mut self. Nada de surpresas sobre para onde o this aponta |
Structs |
Desestruturação const { x, y } = point |
Padrões: let Point { x, y } = point; (também no match e nos parâmetros de funções) |
Pattern matching |
Spread { ...user, admin: true } |
Sintaxe de atualização de struct: User { admin: true, ..user } |
Structs |
switch, ou cadeias de if sobre typeof / kind |
match (o compilador confere se você tratou todos os casos) |
Match |
=== |
==, que compara valores (pelo trait PartialEq). Nunca há coerção de tipos |
Traits comuns |
for...of |
for item in &items |
For em Rust |
map, filter, reduce, find, some, every |
Métodos de iterador: map, filter, fold, find, any, all e depois collect |
Iteradores |
Generators (function*) e Symbol.iterator |
Implemente o trait Iterator |
Iteradores personalizados |
Promise<T> |
Um Future com Output = T. Diferente de uma Promise, ele não faz nada até receber um await |
Async/Await |
async / await |
async fn e .await, mais um runtime como o tokio para executá-los |
Async/Await, Async a fundo |
Promise.all |
tokio::join!, ou join_all do crate futures |
Async a fundo |
setTimeout |
tokio::time::sleep em código async, std::thread::sleep fora dele |
Async/Await |
Web Workers, worker_threads |
Threads de verdade: std::thread::spawn, ou rayon para laços paralelos |
Concorrência |
Módulos ES: import, export |
Módulos: mod, use e pub |
Módulos |
JSON.parse / JSON.stringify |
serde_json com #[derive(Serialize, Deserialize)] |
JSON com Serde |
console.log |
println!, e dbg! para depurar rapidinho. Use {:?} para tipos que derivam Debug |
Saída em Rust |
readonly, as const, Object.freeze |
Não é preciso: tudo é somente leitura, a não ser que você escreva mut |
Variáveis |
| Decorators | Atributos como #[derive(Debug)] e macros |
Macros |
Number("abc") dá NaN |
"abc".parse::<i32>() devolve um Err que você precisa tratar |
Conversão de tipos, Conversões |
| Garbage collector | Ownership e borrowing, verificados em tempo de compilação | Ownership, Borrowing |
Coleções
Seção intitulada “Coleções”| JavaScript | Rust | Saiba mais |
|---|---|---|
Array |
Vec<T> (todos os itens têm o mesmo tipo) |
Vetores |
Array de tamanho fixo, ou uma tupla do TS [string, number] |
[T; N] para um tamanho fixo, uma tupla (String, i32) para tipos misturados |
Arrays, Tuplas |
Map, ou um objeto usado como dicionário (Record<string, T>) |
HashMap<K, V> |
HashMap |
| Um mapa com as chaves ordenadas | BTreeMap<K, V> |
HashMap |
Set |
HashSet<T> (ou BTreeSet<T> para manter a ordem) |
HashSet |
Array usado como fila (push + shift) |
VecDeque<T> (push_back / pop_front, os dois rápidos) |
VecDeque e BinaryHeap |
Array usado como pilha (push + pop) |
Vec<T> com push / pop |
Vetores |
Uint8Array, Buffer |
Vec<u8>, ou um slice de bytes &[u8] |
Slices |
WeakMap, WeakRef |
Weak<T> (a partir de Rc ou Arc) |
Weak e Cow |
Um cuidado: um Map, um Set e um objeto comum do JavaScript lembram a ordem em que você inseriu as coisas. Um HashMap e um HashSet do Rust não. Se você precisa de ordem alfabética, use BTreeMap; se precisa da ordem de inserção, o crate indexmap dá exatamente isso.
Ferramentas: do npm para o cargo
Seção intitulada “Ferramentas: do npm para o cargo”O Cargo vai parecer a sua casa. Ele é gerenciador de pacotes, ferramenta de build, executor de testes e executor de scripts ao mesmo tempo, e todo projeto Rust usa o Cargo do mesmo jeito.
| JavaScript | Rust | Observações |
|---|---|---|
node + npm (ou pnpm, yarn) |
cargo |
Compila, executa, testa, adiciona pacotes, publica. Veja Cargo e crates |
package.json |
Cargo.toml |
|
package-lock.json |
Cargo.lock |
Criado para você |
node_modules/ |
Não há pasta por projeto. Os downloads ficam em cache uma vez só, em ~/.cargo, e o resultado do build vai para target/ |
|
| npmjs.com | crates.io | A documentação da API de todo crate publicado fica no docs.rs |
npm install zod |
cargo add serde |
|
npm init |
cargo new my_app (cargo new --lib para uma biblioteca) |
|
npm run dev / build / test |
cargo run / cargo build / cargo test |
Acrescente --release para um build otimizado; builds de debug são bem mais lentos |
"scripts" no package.json |
Não existe uma seção de scripts. Use aliases do cargo em .cargo/config.toml, ou um executor de tarefas como o just |
|
npx some-tool |
cargo install some-tool e depois rode a ferramenta |
O cargo binstall baixa binários prontos em vez de compilar |
nvm e .nvmrc |
rustup e um arquivo rust-toolchain.toml |
Veja Ferramentas do Rust |
| Workspaces do npm / pnpm (monorepo) | Um workspace do Cargo | Um Cargo.toml no topo lista os crates membros |
tsc |
rustc, chamado para você pelo cargo |
|
tsc --noEmit |
cargo check |
Verifica os tipos sem gerar o build; bem mais rápido |
| ESLint | cargo clippy |
Centenas de lints úteis, oficial |
| Prettier | cargo fmt |
Um estilo padrão para todo mundo |
nodemon, tsx watch |
cargo watch ou bacon |
Instalados com cargo install |
| Jest / Vitest / Mocha | #[test] embutido e cargo test (o cargo nextest é um executor popular e mais rápido) |
Veja Testes em Rust |
| Snapshots do Jest | insta |
Veja Testes avançados |
jest.mock() |
mockall, ou fakes escritos à mão que implementam o seu trait |
|
| fast-check | proptest |
Testes baseados em propriedades |
| c8 / Istanbul (cobertura) | cargo llvm-cov |
|
| JSDoc / TypeDoc | Comentários de documentação /// em Markdown + cargo doc |
Os exemplos de código na documentação rodam como testes. Veja Documentação |
| VS Code + o servidor do TypeScript | VS Code + rust-analyzer, ou JetBrains RustRover | O rust-analyzer oferece autocompletar, ir para a definição e erros na linha |
npm publish |
cargo publish |
Aqui está um Cargo.toml pequeno, para você comparar com um 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"] }Uma versão como "1" significa o mesmo que "^1" no package.json: qualquer versão 1.x compatível. Features são partes opcionais de um crate que você liga; veja Features do Cargo e cfg.
Bibliotecas e frameworks: “O que usamos no lugar de…?”
Seção intitulada “Bibliotecas e frameworks: “O que usamos no lugar de…?””Todo desenvolvedor JavaScript pergunta isso primeiro. “A gente usa Express e Prisma. Qual é a versão em Rust?” Aqui estão os equivalentes mais próximos. Como no npm, o crates.io tem muitas opções para quase tudo; estas são as populares e bem mantidas.
Web e rede
Seção intitulada “Web e rede”| JavaScript | Rust | Observações |
|---|---|---|
| Express, Fastify, Koa | axum (construímos uma API com ele em Projeto: API web com Axum), actix-web, Rocket | axum e actix-web são muito usados em produção |
| Middleware do Express | Camadas do tower e o crate tower-http (CORS, compressão, tracing, timeouts) |
O axum usa o tower diretamente |
fetch, axios |
reqwest |
Assíncrono por padrão, com um cliente bloqueante opcional. Veja Redes em Rust |
ws, Socket.IO |
WebSockets no axum (a feature ws), tokio-tungstenite, ou socketioxide (um servidor Socket.IO) |
|
| Apollo Server (GraphQL) | async-graphql |
|
gRPC (@grpc/grpc-js) |
tonic (com prost para protobuf) |
|
| Geradores de Swagger / OpenAPI | utoipa |
|
jsonwebtoken |
jsonwebtoken |
Mesmo nome, mesma ideia |
bcrypt |
argon2 ou bcrypt |
Acesso a dados
Seção intitulada “Acesso a dados”| JavaScript | Rust | Observações |
|---|---|---|
| Prisma, TypeORM | SeaORM (assíncrono, entidades, migrations) ou Diesel (consultas com tipos verificados em tempo de compilação) | O SeaORM é o que mais lembra o Prisma e o TypeORM |
Knex, pg, postgres.js |
sqlx: você escreve SQL puro, e a macro query! confere a consulta com o seu banco de dados real em tempo de compilação |
Assíncrono, muito popular, não é um ORM |
better-sqlite3 |
rusqlite, ou sqlx com SQLite |
|
| Mongoose, o driver do MongoDB | mongodb (o driver oficial) |
Sem schemas no estilo do Mongoose; você usa structs com serde |
| ioredis | redis |
Bancos de dados em Rust mostra o sqlx, o Diesel e o SeaORM com código que funciona.
Blocos do dia a dia
Seção intitulada “Blocos do dia a dia”| JavaScript | Rust | Observações |
|---|---|---|
JSON.parse / JSON.stringify |
serde + serde_json |
O padrão; também lida com TOML, YAML e mais. Veja JSON com Serde |
| Zod, Yup | Os tipos da sua struct + serde fazem a maior parte do trabalho; validator ou garde para regras como “pelo menos 8 caracteres” |
Se o JSON não bate com a struct, o parsing falha com um erro |
| Lodash | Quase sempre os iteradores da biblioteca padrão; itertools para extras como unique, sorted_by e chunk_by |
|
| date-fns, Day.js, Luxon | chrono ou jiff |
Veja Datas e horas |
| dotenv | dotenvy |
Veja Ambiente e processos |
| Commander, yargs | clap |
Veja Projeto: CLI com clap |
| chalk | colored ou owo-colors |
|
| winston, pino | tracing (+ tracing-subscriber), ou log + env_logger |
Veja Logging |
uuid |
uuid |
|
EventEmitter |
Canais (std::sync::mpsc, ou tokio::sync::broadcast para muitos ouvintes) |
Veja Canais (channels) |
| BullMQ (filas de jobs) | tokio::spawn para tarefas dentro do processo; apalis para filas de jobs persistentes |
|
| Expressões regulares | regex |
Sem lookahead nem backreferences, de propósito. Veja Expressões regulares |
Frontend, desktop e Node.js
Seção intitulada “Frontend, desktop e Node.js”| JavaScript | Rust | Observações |
|---|---|---|
| Acelerar código pesado no navegador | Compile Rust para WebAssembly com wasm-bindgen e wasm-pack |
Veja WebAssembly e a seção mais abaixo |
| React, Vue, Svelte | Leptos, Yew, Dioxus (todos compilam para WebAssembly) |
Todos antes da versão 1.0; as APIs ainda mudam |
| Electron | Tauri: o seu frontend web + um backend em Rust, com apps bem menores |
O Tauri 2 é estável. Veja Apps com interface gráfica |
| Addons nativos do Node.js (C++ com node-gyp) | napi + napi-derive (napi-rs), ou neon |
Publique binários prontos no npm; sem precisar de C++ |
| Jogos com Phaser, Three.js | Bevy, macroquad |
O Bevy está antes da 1.0 e quebra a API entre versões. Veja Desenvolvimento de jogos |
Lado a lado: uma interface vira uma struct
Seção intitulada “Lado a lado: uma interface vira uma struct”Aqui está um objeto TypeScript com uma interface, uma função que faz papel de método e uma cópia com 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);Em Rust, o formato vira uma struct, e a função vira um método num bloco impl:
#[derive(Debug, Clone)]struct User { name: String, age: u32, admin: bool,}
impl User { // `&self` significa "este método lê o usuário, não o altera". fn greeting(&self) -> String { format!("Oi, eu sou {} ({})", self.name, self.age) }}
fn main() { let ada = User { name: "Ada".to_string(), age: 36, admin: false };
// Como o { ...ada, admin: true } do JavaScript. let promoted = User { admin: true, ..ada.clone() };
println!("{}", ada.greeting()); println!("{:?}", promoted);}Saída:
Oi, eu sou Ada (36)User { name: "Ada", age: 36, admin: true }Algumas coisas mudam. Cada campo tem um tipo exato: age é um u32 (um número inteiro que não pode ser negativo), e não um number genérico. O #[derive(Debug)] é o que permite imprimir a struct inteira com {:?}, como um console.log(obj). E por que o .clone()? O spread ..ada moveria a string name para fora de ada, e aí não poderíamos mais usar ada. Clonar antes mantém ada intacto. Isso é o ownership em ação, e você vai ver muito isso.
Lado a lado: uma discriminated union vira um enum
Seção intitulada “Lado a lado: uma discriminated union vira um enum”Quem programa em TypeScript adora discriminated unions: um campo kind diz qual formato você tem, e o switch estreita o tipo:
type Shape = | { kind: "circle"; radius: number } | { kind: "rect"; width: number; height: number };
function area(s: Shape): number { switch (s.kind) { case "circle": return Math.PI * s.radius ** 2; case "rect": return s.width * s.height; }}
const shapes: Shape[] = [ { kind: "circle", radius: 1 }, { kind: "rect", width: 2, height: 3 },];for (const s of shapes) console.log(area(s).toFixed(2));O Rust já traz isso embutido, e é um dos melhores recursos da linguagem. Cada variante de um enum carrega os próprios dados, e o match desmonta o valor:
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)); }}Saída:
3.146.00Não é preciso uma string kind: a própria variante é a etiqueta. E o match precisa cobrir todas as variantes. Se você adicionar um Triangle depois, todo match que esquecer dele vira um erro de compilação, como o truque de exaustividade com never do TypeScript, só que sempre ligado. Mais sobre isso em Pattern matching.
Lado a lado: undefined e null viram Option
Seção intitulada “Lado a lado: undefined e null viram Option”Em TypeScript, valores opcionais aparecem como undefined, null ou uma propriedade opcional, e você usa ?. e ?? para lidar com eles:
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"}`);}Em Rust, um valor que pode estar faltando tem o tipo Option<T>. Ele é ou Some(valor) ou None, e você não consegue usá-lo como um valor comum antes de tratar o caso None:
struct User { name: String, email: Option<String>,}
fn find_user(id: u32) -> Option<User> { match id { 2 => Some(User { name: "Grace".to_string(), email: None }), _ => None, }}
// Dentro de uma função que devolve Option, o `?` funciona como o `?.`:// se o valor for None, pare aqui e devolva 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] { // Como findUser(id)?.email?.toUpperCase() ?? "no email" let email = find_user(id) .and_then(|user| user.email) .map(|email| email.to_uppercase()) .unwrap_or("sem e-mail".to_string()); println!("{id}: {email}"); }
println!("{:?}", email_domain(1)); println!("{:?}", email_domain(2));
if let Some(user) = find_user(2) { println!("encontrei {}", user.name); }}Saída:
2: sem e-mail3: sem e-mailSome("example.com")Noneencontrei GraceNão existe “cannot read properties of undefined” no Rust, porque não existe undefined para ler. O compilador simplesmente não deixa você chamar .to_uppercase() em um Option<String>. E não há diferença entre null e undefined para decorar: só existe None. Aprenda os métodos auxiliares na página de Option, e as formas curtas em If Let / While Let.
Lado a lado: try/catch vira Result e ?
Seção intitulada “Lado a lado: try/catch vira Result e ?”Em JavaScript, uma função lança um erro, e você o captura em algum lugar mais acima na pilha de chamadas. Nada na assinatura da função conta que ela pode lançar:
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}`); }}Em Rust, uma função que pode falhar devolve um Result: ou Ok(valor) ou Err(erro). O operador ? significa “se isto falhou, devolva o erro para quem me chamou agora mesmo”. Ele faz o mesmo trabalho de deixar uma exceção subir, mas você consegue vê-lo no código:
fn parse_age(input: &str) -> Result<u32, String> { let age: u32 = input .parse() .map_err(|_| format!("'{input}' não é um número inteiro"))?;
if age > 150 { return Err(format!("{age} é uma idade alta demais")); } Ok(age)}
fn total_age(a: &str, b: &str) -> Result<u32, String> { // Cada `?` repassa um erro para quem nos chamou, como faria uma exceção. 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!("Erro: {e}"), } }}Saída:
Total: 77Erro: 'abc' não é um número inteiroErro: 200 é uma idade alta demaisA assinatura -> Result<u32, String> avisa a todo mundo que chama “isto pode falhar”, e o compilador não deixa ninguém ignorar isso. O mesmo vale para código assíncrono: onde você teria uma Promise rejeitada no JavaScript, uma função async do Rust devolve um Result, e você escreve .await?. Para criar os seus próprios tipos de erro, veja Erros personalizados. Em aplicações, o crate anyhow oferece algo parecido com “capturar qualquer erro” (veja anyhow e thiserror).
Lado a lado: métodos de array viram iteradores
Seção intitulada “Lado a lado: métodos de array viram iteradores”Os métodos de array que você usa todo dia têm equivalentes diretos no 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)}`);A versão em Rust usa iteradores e closures. A sintaxe da closure é |o| o.paid em vez de (o) => o.paid:
struct Order { customer: String, total: u32, paid: bool,}
fn main() { let orders = vec![ Order { customer: "Ada".to_string(), total: 120, paid: true }, Order { customer: "Grace".to_string(), total: 80, paid: false }, Order { customer: "Linus".to_string(), total: 200, paid: true }, ];
// filter + map, e depois o collect() transforma o resultado num Vec. let paid_names: Vec<&str> = orders .iter() .filter(|o| o.paid) .map(|o| o.customer.as_str()) .collect();
// O reduce com valor inicial se chama fold. let revenue = orders .iter() .filter(|o| o.paid) .fold(0, |sum, o| sum + o.total);
// find devolve um Option, porque pode não haver nenhum resultado. let first_big = orders.iter().find(|o| o.total > 150);
println!("{}", paid_names.join(", ")); println!("Faturamento: {revenue}"); match first_big { Some(o) => println!("Primeiro pedido grande: {}", o.customer), None => println!("Primeiro pedido grande: nenhum"), } // O some() se chama any(). println!("Algum pedido não pago? {}", orders.iter().any(|o| !o.paid));}Saída:
Ada, LinusFaturamento: 320Primeiro pedido grande: LinusAlgum pedido não pago? trueHá uma grande diferença em relação ao JavaScript. No JS, cada método de array monta um array novo inteiro na hora. Os iteradores do Rust são preguiçosos (lazy): filter e map não fazem nada sozinhos, e o trabalho acontece numa única passada quando você chama algo como collect(), sum(), fold() ou um laço for. Nenhum array temporário é criado no meio do caminho. Só para a soma, você também poderia escrever .map(|o| o.total).sum::<u32>(). A ordenação fica no Vec (sort, sort_by), e não nos iteradores; veja Ordenação e busca.
Lado a lado: Promises viram futures + tokio
Seção intitulada “Lado a lado: Promises viram futures + tokio”Aqui está um pouco de JavaScript assíncrono. Repare em quando "fetching 1" é impresso:
async function fetchUser(id) { console.log(`fetching ${id}`); await new Promise((resolve) => setTimeout(resolve, 100)); return `user ${id}`;}
const promise = fetchUser(1); // imprime "fetching 1" na horaconsole.log("promise created");console.log(await promise);
const [a, b] = await Promise.all([fetchUser(2), fetchUser(3)]);console.log(`${a}, ${b}`);No JavaScript, chamar uma função async já a inicia na hora. O Rust é diferente: chamar uma async fn só cria um future, uma descrição do trabalho a fazer. Nada roda até você fazer .await nele. E o Rust não tem um event loop embutido, então você adiciona um runtime: o tokio é o mais popular.
use std::time::Duration;
async fn fetch_user(id: u32) -> String { println!("buscando {id}"); tokio::time::sleep(Duration::from_millis(100)).await; format!("usuário {id}")}
#[tokio::main]async fn main() { // Criar um future NÃO o inicia. Nada é impresso ainda. let future = fetch_user(1); println!("future criado"); println!("{}", future.await);
// Como o Promise.all: roda os dois ao mesmo tempo e espera os dois. let (a, b) = tokio::join!(fetch_user(2), fetch_user(3)); println!("{a}, {b}");}Saída:
future criadobuscando 1usuário 1buscando 2buscando 3usuário 2, usuário 3Viu a ordem? No JavaScript, “fetching 1” apareceria antes de “promise created”. No Rust, “buscando 1” aparece depois de “future criado”, porque o future só começa no .await. O #[tokio::main] prepara o runtime (o seu event loop) e roda o main nele. O tokio::join! espera os dois futures juntos, como o Promise.all; o tokio::spawn inicia uma tarefa em segundo plano, que é o mais perto de chamar uma função async sem fazer await. As lições Async/Await e Async a fundo explicam os detalhes.
Rust no seu projeto JavaScript: WebAssembly e addons do Node
Seção intitulada “Rust no seu projeto JavaScript: WebAssembly e addons do Node”Você não precisa reescrever nada para começar a usar Rust. Há dois jeitos comuns de misturá-lo num projeto JavaScript:
WebAssembly no navegador. Marque funções Rust com #[wasm_bindgen], compile com o wasm-pack e importe o resultado como qualquer módulo ES. O wasm-pack até gera um arquivo .d.ts, para o TypeScript conhecer os tipos das suas funções:
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(); // baixa e inicia o arquivo .wasmconsole.log(slugify("Hello Rust World")); // "hello-rust-world"A lição de WebAssembly mostra a configuração completa, passo a passo, e explica quais valores atravessam a fronteira sem custo alto. Uma dica: o Wasm compensa para computação pesada (processamento de imagens, parsing, simulações), e não para funções pequenas que você chama milhares de vezes, porque cada chamada entre JS e Wasm tem um pequeno custo.
Addons nativos do Node.js. Com o napi-rs (os crates napi e napi-derive), você marca uma função Rust com #[napi], e ela vira uma função que você pode usar com require ou import no Node. Muitos pacotes populares do npm distribuem binários Rust prontos desse jeito, então os usuários nunca precisam de um compilador Rust.
O que vai fazer você tropeçar vindo do JavaScript
Seção intitulada “O que vai fazer você tropeçar vindo do JavaScript”Todo mundo que vem do JavaScript passa pelos mesmos buracos. Conhecer todos com antecedência ajuda.
Passar um valor para uma função o move. Em JavaScript, passar um array para uma função passa uma referência, e você continua podendo usar o array depois. Em Rust, passar um Vec ou uma String por valor move o ownership para dentro da função, e a variável original não pode mais ser usada:
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 moveO compilador diz (em inglês) que names foi movido na linha 9 (“value moved here”) e depois usado na linha 10 (“value borrowed here after move”). A correção mais comum é pegar emprestado: faça a função receber names: &[String] e chame com print_all(&names). Leia primeiro Ownership e Borrowing; essas páginas explicam essa regra, e você vai encontrá-la todo dia.
Números têm tipos, e eles não se misturam. O JavaScript tem um único number. No Rust, 3 é um inteiro e 2.5 é um float, e você não pode combiná-los sem uma conversão:
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}`A mensagem diz que não dá para multiplicar um inteiro por um float. Escreva items as f64 * price, ou faça de items um float desde o início (3.0). Repare também que a divisão inteira descarta o resto: 7 / 2 dá 3, e não 3.5. Veja Tipos de dados e Conversão de tipos.
Mais alguns que você vai encontrar:
- Futures são preguiçosos e precisam de um runtime. Como você viu acima, nada acontece até o
.await, e você precisa do tokio (ou de outro runtime) para rodar código async. Esquecer o.awaitgera um aviso do compilador, e não uma tarefa rodando. - Strings não podem ser indexadas.
name[0]não compila, porque umaStringdo Rust é UTF-8 e um caractere pode ocupar vários bytes. Usename.chars().next()ou.chars().nth(i). E aprenda logo a diferença entreStringe&str: funções normalmente recebem&stre structs normalmente guardamString. Veja Strings. - Sem tipagem estrutural e sem
any. No TypeScript, qualquer objeto com os campos certos serve para uma interface. No Rust, uma função que recebe umUsersó aceita umUser. Para comportamento compartilhado, use traits. Para um JSON cujo formato você não conhece, useserde_json::Value. - Grafos de objetos compartilhados e mutáveis são difíceis. No JavaScript é normal passar um objeto por aí e deixar todo mundo alterá-lo, ou ter objetos que apontam de volta para o pai. O borrow checker permite muitos leitores ou um único escritor ao mesmo tempo, nunca os dois. Padrões assim precisam de smart pointers (ponteiros inteligentes, como
Rc<RefCell<T>>eArc<Mutex<T>>) ou, melhor ainda, de outro design. Prepare-se para algumas semanas frustrantes. Depois fica bem mais fácil. - O
HashMapnão tem ordem.Maps e objetos do JavaScript mantêm a ordem de inserção, então é comum o código depender disso sem perceber. UmHashMapdo Rust não mantém. Ordene antes de imprimir, ou useBTreeMapou o crateindexmap. - Sem exceções. Erros são valores, e você vai escrever
?o tempo todo. Chamar.unwrap()é tranquilo enquanto você aprende, mas é a versão Rust do!do TypeScript: “confia em mim, o valor está aí”. Se você estiver errado, o programa entra em panic. - Tempo de compilação. Não existe o recarregamento instantâneo de um
node app.js. O primeiro build com muitos crates demora um pouco; os seguintes são incrementais e mais rápidos. Use ocargo checkenquanto edita, já que ele pula a geração de código.
Uma trilha de estudo para desenvolvedores JavaScript
Seção intitulada “Uma trilha de estudo para desenvolvedores JavaScript”Você não precisa ler este site na ordem. Como desenvolvedor JavaScript ou TypeScript, este caminho pula o que você já sabe e investe tempo onde o Rust é diferente:
- Começando com Rust e Ferramentas do Rust: instale o Rust, configure o VS Code com o rust-analyzer e rode
cargo new. - Variáveis e Tipos de dados: leia com atenção. Os tipos numéricos são novidade para você.
- Ownership, depois Borrowing, depois Slices: as páginas mais importantes para você. Vá com calma.
- Strings:
Stringvs&str. - Structs e Enums, depois Match e Pattern matching: as suas interfaces e discriminated unions.
- Option e Result, depois Erros personalizados.
- Closures e Iteradores: os seus métodos de array.
- Vetores, HashMap e HashSet.
- Traits, Genéricos e Traits comuns.
- Módulos, Cargo e crates e Testes em Rust.
- Lifetimes e Ponteiros inteligentes.
- Async/Await e Async a fundo.
- JSON com Serde, e depois construa o projeto de API web, ou experimente o WebAssembly.
Prática
Seção intitulada “Prática”Os dois exercícios trazem um trecho de JavaScript ou TypeScript. Converta para Rust.
1. Conte palavras com um Map
Seção intitulada “1. Conte palavras com um Map”Este código JavaScript conta quantas vezes cada palavra aparece e depois imprime as palavras da mais comum para a menos comum (empates em ordem alfabética). Converta para Rust. Dica: use um HashMap<&str, u32> e *counts.entry(word).or_insert(0) += 1. Para ordenar, colete o mapa num Vec de pares (palavra, contagem) e use 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}`);Ver solução
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; }
// Como o [...counts]: transforma o mapa numa lista de pares (palavra, contagem). let mut sorted: Vec<(&str, u32)> = counts.into_iter().collect(); // As mais comuns primeiro; empates em ordem alfabética. sorted.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (word, n) in sorted { println!("{word}: {n}"); }}Saída:
the: 3and: 2bat: 1cat: 1hat: 1O entry(word).or_insert(0) faz a parte do counts.get(word) ?? 0 e o set num passo só: ele dá uma referência mutável para a contagem, criando-a com 0 se ela ainda não existir. A ordenação é essencial aqui, não um mero detalhe: sem ela, um HashMap imprime numa ordem diferente a cada execução. (O texto ficou em inglês porque a saída depende da ordem alfabética.)
2. Converta opções com valores padrão
Seção intitulada “2. Converta opções com valores padrão”Esta função TypeScript recebe um objeto de opções em que todos os campos são opcionais e preenche os valores padrão com ??. Converta para Rust. Dica: crie uma struct com campos Option e adicione #[derive(Default)]. Aí ServerOptions { port: Some(3000), ..Default::default() } funciona como passar { port: 3000 }. Para um bool, o padrão é 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 }));Ver solução
#[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 { "detalhado" } else { "silencioso" }; 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() }) );}Saída:
localhost:8080 (silencioso)localhost:3000 (silencioso)example.com:8080 (detalhado)O unwrap_or é o seu ??. O as_deref() transforma um Option<String> em um Option<&str>, para poder cair no texto simples "localhost" sem copiar nada. E o ..Default::default() é o jeito Rust de dizer “todo campo que eu não mencionei recebe o valor padrão”, que é exatamente o que deixar uma propriedade de fora faz no JavaScript. Usar u16 para a porta também significa que uma porta como 70000 nem consegue ser guardada por engano.
- Muita coisa do JavaScript e do TypeScript vem junto: closures, async/await, desestruturação e métodos no estilo dos arrays, como
map,filterefind, têm equivalentes bem próximos no Rust. - As grandes mudanças são ownership no lugar de um garbage collector,
Optionno lugar deundefinedenull, eResult+?no lugar dethrowetry/catch. - Interfaces do TypeScript viram structs (para dados) ou traits (para comportamento), e discriminated unions viram enums com
match. - Os futures do Rust são preguiçosos e precisam de um runtime como o tokio; o
tokio::join!faz o papel doPromise.all. - O Cargo substitui o npm, o
package.jsone os seus scripts; o clippy e o rustfmt substituem o ESLint e o Prettier; os testes já vêm embutidos. - No lugar do Express, veja o axum ou o actix-web; no lugar do
fetch, o reqwest; no lugar do Prisma, o SeaORM ou o Diesel; e no lugar do Electron, o Tauri. Você também pode trazer Rust para um projeto JS com WebAssembly ou napi-rs.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. O que o Rust usa no lugar de undefined e null?
2. No JavaScript, chamar uma função async já a inicia. O que acontece quando você chama uma async fn no Rust sem .await?
3. O seu código TypeScript usa uma discriminated union com um campo kind. Qual é o recurso mais próximo no Rust?
4. O backend do seu time usa Express. Qual crate do Rust é o mais parecido?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- The Rust Programming Language (em inglês): o livro oficial. Os capítulos 4 (ownership), 6 (enums e pattern matching) e 9 (erros) são os que mais importam para quem vem do JavaScript.
- Tokio tutorial (em inglês): o guia oficial de Rust assíncrono com o tokio, da primeira tarefa até canais e I/O.
- The wasm-bindgen Guide (em inglês): como Rust e JavaScript conversam no WebAssembly, com muitos exemplos.
- Compiling from Rust to WebAssembly (MDN) (em inglês): um tutorial passo a passo da MDN Web Docs, incluindo a publicação no npm.
- napi-rs (em inglês): a documentação para escrever addons do Node.js em Rust.