Pular para o conteúdo

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

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.

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

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

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 }
▶ Experimente você mesmo

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.

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.14
6.00
▶ Experimente você mesmo

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

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 === 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"}`);
}

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 {
1 => Some(User { name: "Ada".to_string(), email: Some("[email protected]".to_string()) }),
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-mail
3: sem e-mail
Some("example.com")
None
encontrei Grace
▶ Experimente você mesmo

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

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: 77
Erro: 'abc' não é um número inteiro
Erro: 200 é uma idade alta demais
▶ Experimente você mesmo

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

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, Linus
Faturamento: 320
Primeiro pedido grande: Linus
Algum pedido não pago? true
▶ Experimente você mesmo

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

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 hora
console.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 criado
buscando 1
usuário 1
buscando 2
buscando 3
usuário 2, usuário 3
▶ Experimente você mesmo

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

O 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 .await gera um aviso do compilador, e não uma tarefa rodando.
  • Strings não podem ser indexadas. name[0] não compila, porque uma String do Rust é UTF-8 e um caractere pode ocupar vários bytes. Use name.chars().next() ou .chars().nth(i). E aprenda logo a diferença entre String e &str: funções normalmente recebem &str e structs normalmente guardam String. 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 um User só aceita um User. Para comportamento compartilhado, use traits. Para um JSON cujo formato você não conhece, use serde_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>> e Arc<Mutex<T>>) ou, melhor ainda, de outro design. Prepare-se para algumas semanas frustrantes. Depois fica bem mais fácil.
  • O HashMap nã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. Um HashMap do Rust não mantém. Ordene antes de imprimir, ou use BTreeMap ou o crate indexmap.
  • 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 o cargo check enquanto 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:

  1. Começando com Rust e Ferramentas do Rust: instale o Rust, configure o VS Code com o rust-analyzer e rode cargo new.
  2. Variáveis e Tipos de dados: leia com atenção. Os tipos numéricos são novidade para você.
  3. Ownership, depois Borrowing, depois Slices: as páginas mais importantes para você. Vá com calma.
  4. Strings: String vs &str.
  5. Structs e Enums, depois Match e Pattern matching: as suas interfaces e discriminated unions.
  6. Option e Result, depois Erros personalizados.
  7. Closures e Iteradores: os seus métodos de array.
  8. Vetores, HashMap e HashSet.
  9. Traits, Genéricos e Traits comuns.
  10. Módulos, Cargo e crates e Testes em Rust.
  11. Lifetimes e Ponteiros inteligentes.
  12. Async/Await e Async a fundo.
  13. JSON com Serde, e depois construa o projeto de API web, ou experimente o WebAssembly.

Os dois exercícios trazem um trecho de JavaScript ou TypeScript. Converta para Rust.

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: 3
and: 2
bat: 1
cat: 1
hat: 1
▶ Experimente você mesmo

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

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)
▶ Experimente você mesmo

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, filter e find, têm equivalentes bem próximos no Rust.
  • As grandes mudanças são ownership no lugar de um garbage collector, Option no lugar de undefined e null, e Result + ? no lugar de throw e try/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 do Promise.all.
  • O Cargo substitui o npm, o package.json e 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

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

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