Se você é um desenvolvedor JavaScript (ou TypeScript) e quer aprender Rust, este guia é para você. Vamos traduzir os conceitos que você já domina do ecossistema JS para seus equivalentes em Rust, mostrando lado a lado como o código se compara. Rust pode parecer muito diferente no início, mas muitos conceitos fundamentais possuem paralelos diretos.
Variáveis: let/const vs let/let mut
Ambas as linguagens usam let, mas com semânticas diferentes.
| Conceito | JavaScript | Rust |
|---|---|---|
| Imutável | const x = 10; | let x = 10; |
| Mutável | let x = 10; | let mut x = 10; |
| Constante global | const PI = 3.14; | const PI: f64 = 3.14; |
| Sem tipo declarado | let x = 10; | let x = 10; (tipo inferido) |
| Com tipo declarado | let x: number = 10; (TS) | let x: i32 = 10; |
JavaScript:
let nome = "Maria";
nome = "Ana"; // ok, let permite reatribuição
const idade = 30;
// idade = 31; // erro: const não permite reatribuição
let valor = 42;
valor = "texto"; // JS permite mudar o tipo
Rust:
let mut nome = String::from("Maria");
nome = String::from("Ana"); // ok, mut permite reatribuição
let idade = 30;
// idade = 31; // erro: sem mut, não permite reatribuição
let valor = 42;
// valor = "texto"; // ERRO: tipos incompatíveis
// Shadowing permite "redeclarar"
let valor = "texto"; // ok, novo binding com shadowing
Em Rust, let (sem mut) se comporta como const do JavaScript. E let mut se comporta como let do JavaScript. Porém, diferente do JS, Rust nunca permite mudar o tipo de uma variável mutável – apenas shadowing permite isso.
undefined/null vs Option<T>
JavaScript tem dois tipos para “ausência de valor”: undefined e null. Rust unifica isso em Option<T>, verificado em tempo de compilação.
| Conceito | JavaScript | Rust |
|---|---|---|
| Sem valor | undefined / null | None |
| Com valor | valor direto | Some(valor) |
| Checagem | if (x != null) | if let Some(v) = x |
| Acesso seguro | x?.prop (optional chaining) | x.map(|v| v.prop) |
| Valor padrão | x ?? "default" | x.unwrap_or("default") |
JavaScript:
function buscarUsuario(id) {
const usuarios = { 1: "Ana", 2: "Bruno" };
return usuarios[id] ?? null;
}
const nome = buscarUsuario(3);
if (nome !== null) {
console.log(`Encontrado: ${nome}`);
} else {
console.log("Não encontrado");
}
// Optional chaining
const tamanho = nome?.length ?? 0;
Rust:
use std::collections::HashMap;
fn buscar_usuario(id: u32) -> Option<String> {
let mut usuarios = HashMap::new();
usuarios.insert(1, String::from("Ana"));
usuarios.insert(2, String::from("Bruno"));
usuarios.get(&id).cloned()
}
// Pattern matching
match buscar_usuario(3) {
Some(nome) => println!("Encontrado: {}", nome),
None => println!("Não encontrado"),
}
// Equivalente ao ?? (nullish coalescing)
let tamanho = buscar_usuario(3)
.map(|n| n.len())
.unwrap_or(0);
A grande vantagem do Option<T>: o compilador Rust garante que você trate o caso None. Em JavaScript, esquecer de checar null ou undefined causa erros em runtime como TypeError: Cannot read property of undefined.
try/catch vs Result<T, E>
JavaScript usa exceções com try/catch. Rust usa o tipo Result<T, E> com o operador ?.
JavaScript:
async function carregarDados(url) {
try {
const resposta = await fetch(url);
if (!resposta.ok) {
throw new Error(`HTTP ${resposta.status}`);
}
const dados = await resposta.json();
return dados;
} catch (erro) {
console.error("Falha ao carregar:", erro.message);
return null;
}
}
Rust:
use reqwest;
use serde::Deserialize;
#[derive(Deserialize)]
struct Dados {
titulo: String,
}
async fn carregar_dados(url: &str) -> Result<Dados, reqwest::Error> {
let dados = reqwest::get(url)
.await? // propaga erro de conexão
.json::<Dados>()
.await?; // propaga erro de parse
Ok(dados)
}
// Uso
#[tokio::main]
async fn main() {
match carregar_dados("https://api.exemplo.com/dados").await {
Ok(dados) => println!("Título: {}", dados.titulo),
Err(e) => eprintln!("Falha ao carregar: {}", e),
}
}
O operador ? em Rust funciona como um throw automático que propaga o erro para o chamador. A diferença crucial: em JavaScript, qualquer função pode lançar uma exceção silenciosamente. Em Rust, Result no tipo de retorno torna o erro visível e obrigatório de tratar.
Promises vs Futures
Promises do JavaScript e Futures do Rust são conceitualmente similares, mas com diferenças importantes.
| Conceito | JavaScript | Rust (Tokio) |
|---|---|---|
| Criar async | async function f() | async fn f() |
| Aguardar | await promise | future.await |
| Executar em paralelo | Promise.all([...]) | tokio::join!(...) |
| Primeira a resolver | Promise.race([...]) | tokio::select!(...) |
| Timeout | Promise.race([p, timeout]) | tokio::time::timeout() |
JavaScript:
async function buscarDados() {
const [usuarios, posts] = await Promise.all([
fetch("/api/usuarios").then(r => r.json()),
fetch("/api/posts").then(r => r.json()),
]);
return { usuarios, posts };
}
Rust:
async fn buscar_dados() -> Result<(Vec<Usuario>, Vec<Post>), reqwest::Error> {
let (usuarios, posts) = tokio::join!(
async { reqwest::get("/api/usuarios").await?.json().await },
async { reqwest::get("/api/posts").await?.json().await },
);
Ok((usuarios?, posts?))
}
Uma diferença fundamental: em JavaScript, uma Promise começa a executar imediatamente ao ser criada. Em Rust, um Future é lazy – só executa quando alguém faz .await nele ou o submete a um runtime com tokio::spawn.
npm vs Cargo
| Tarefa | JavaScript (npm) | Rust (Cargo) |
|---|---|---|
| Iniciar projeto | npm init | cargo new projeto |
| Arquivo de config | package.json | Cargo.toml |
| Lock file | package-lock.json | Cargo.lock |
| Instalar dependências | npm install | cargo build |
| Adicionar pacote | npm install express | cargo add actix-web |
| Executar | node index.js / npm start | cargo run |
| Testar | npm test (jest/vitest) | cargo test |
| Formatar | npx prettier --write . | cargo fmt |
| Lint | npx eslint . | cargo clippy |
| Build produção | npm run build (webpack, etc.) | cargo build --release |
| Repositório | npmjs.com | crates.io |
Cargo combina as funcionalidades de npm, webpack, jest, prettier e eslint em uma única ferramenta integrada. Não há necessidade de configurar bundlers ou test runners separados.
Objetos vs Structs
Objetos JavaScript são dinâmicos e flexíveis. Structs em Rust são tipados e fixos.
JavaScript:
const usuario = {
nome: "Carlos",
email: "carlos@email.com",
idade: 28,
};
// Adicionar propriedade dinâmica
usuario.ativo = true;
// Destructuring
const { nome, email } = usuario;
// Spread
const atualizado = { ...usuario, idade: 29 };
Rust:
#[derive(Debug, Clone)]
struct Usuario {
nome: String,
email: String,
idade: u32,
}
let usuario = Usuario {
nome: String::from("Carlos"),
email: String::from("carlos@email.com"),
idade: 28,
};
// Não é possível adicionar campos dinamicamente
// Destructuring
let Usuario { nome, email, .. } = &usuario;
// Struct update syntax (similar ao spread)
let atualizado = Usuario {
idade: 29,
..usuario.clone()
};
Prototypes vs Traits
A cadeia de protótipos do JavaScript e as traits de Rust resolvem o mesmo problema: compartilhar comportamento entre tipos diferentes.
JavaScript:
class Animal {
constructor(nome) {
this.nome = nome;
}
}
class Cachorro extends Animal {
falar() {
return `${this.nome} diz: Au au!`;
}
}
class Gato extends Animal {
falar() {
return `${this.nome} diz: Miau!`;
}
}
// Duck typing
function fazerFalar(animal) {
console.log(animal.falar());
}
Rust:
trait Falante {
fn falar(&self) -> String;
}
struct Cachorro {
nome: String,
}
struct Gato {
nome: String,
}
impl Falante for Cachorro {
fn falar(&self) -> String {
format!("{} diz: Au au!", self.nome)
}
}
impl Falante for Gato {
fn falar(&self) -> String {
format!("{} diz: Miau!", self.nome)
}
}
// Polimorfismo via trait
fn fazer_falar(animal: &dyn Falante) {
println!("{}", animal.falar());
}
Rust usa composição em vez de herança. Traits definem contratos explícitos, semelhante a interfaces do TypeScript.
Callbacks vs Closures
Ambas as linguagens suportam closures, mas Rust exige anotar como a closure captura variáveis do ambiente.
JavaScript:
const numeros = [1, 2, 3, 4, 5];
// Map/Filter/Reduce
const resultado = numeros
.filter(n => n % 2 === 0)
.map(n => n * 2)
.reduce((acc, n) => acc + n, 0);
console.log(resultado); // 12
// Callback
function executarComAtraso(callback, ms) {
setTimeout(callback, ms);
}
executarComAtraso(() => console.log("Executado!"), 1000);
Rust:
let numeros = vec![1, 2, 3, 4, 5];
// Map/Filter/Fold (equivalente ao reduce)
let resultado: i32 = numeros.iter()
.filter(|&&n| n % 2 == 0)
.map(|&n| n * 2)
.sum();
println!("{}", resultado); // 12
// Closures como parâmetros
fn executar_com_atraso<F: FnOnce()>(callback: F, ms: u64) {
std::thread::sleep(std::time::Duration::from_millis(ms));
callback();
}
executar_com_atraso(|| println!("Executado!"), 1000);
Rust tem tres tipos de closures: Fn (emprestam dados imutavelmente), FnMut (emprestam dados mutavelmente) e FnOnce (consomem os dados capturados). Em JavaScript, closures sempre capturam por referência, sem distinção.
Tipos TypeScript vs Tipos Rust
Se você usa TypeScript, muitos conceitos de tipos traduzem diretamente para Rust.
| TypeScript | Rust |
|---|---|
number | i32, f64, u32, etc. |
string | String / &str |
boolean | bool |
T[] / Array<T> | Vec<T> |
Record<K, V> | HashMap<K, V> |
T | null | Option<T> |
interface | trait |
type (union) | enum |
generic<T> | <T> |
as (type assertion) | Pattern matching / as (conversão numérica) |
TypeScript:
interface Serializavel {
toJSON(): string;
}
type Resultado<T> =
| { sucesso: true; dados: T }
| { sucesso: false; erro: string };
function processar<T extends Serializavel>(
item: T
): Resultado<string> {
try {
return { sucesso: true, dados: item.toJSON() };
} catch (e) {
return { sucesso: false, erro: String(e) };
}
}
Rust:
use serde::Serialize;
trait Serializavel {
fn to_json(&self) -> Result<String, serde_json::Error>;
}
enum Resultado<T> {
Sucesso(T),
Erro(String),
}
fn processar<T: Serializavel>(item: &T) -> Resultado<String> {
match item.to_json() {
Ok(json) => Resultado::Sucesso(json),
Err(e) => Resultado::Erro(e.to_string()),
}
}
Os enums de Rust substituem as union types do TypeScript de forma mais poderosa, pois cada variante pode carregar dados diferentes e o compilador garante que todos os casos sejam tratados no match.
JSON com serde_json
Trabalhar com JSON é parte do dia a dia em JavaScript. Em Rust, a crate serde com serde_json fornece serialização/desserialização poderosa.
JavaScript:
const json = '{"nome": "Ana", "idade": 25}';
const obj = JSON.parse(json);
console.log(obj.nome);
const novoJson = JSON.stringify({ nome: "Bruno", idade: 30 });
Rust:
use serde::{Deserialize, Serialize};
use serde_json;
#[derive(Serialize, Deserialize, Debug)]
struct Pessoa {
nome: String,
idade: u32,
}
fn main() -> Result<(), serde_json::Error> {
// Desserializar (parse)
let json = r#"{"nome": "Ana", "idade": 25}"#;
let pessoa: Pessoa = serde_json::from_str(json)?;
println!("{}", pessoa.nome);
// Serializar (stringify)
let nova_pessoa = Pessoa {
nome: String::from("Bruno"),
idade: 30,
};
let novo_json = serde_json::to_string(&nova_pessoa)?;
println!("{}", novo_json);
// Também é possível trabalhar com JSON dinâmico
let valor: serde_json::Value = serde_json::from_str(json)?;
println!("{}", valor["nome"]);
Ok(())
}
A abordagem do Rust com serde oferece desserialização tipada – erros de formato são capturados em tempo de parse, não quando você acessa um campo que não existe.
Node.js vs Rust para Backends
Ambas as linguagens são populares para backends. Vamos comparar um servidor HTTP simples.
Node.js (Express):
const express = require("express");
const app = express();
app.use(express.json());
let tarefas = [];
app.get("/tarefas", (req, res) => {
res.json(tarefas);
});
app.post("/tarefas", (req, res) => {
const tarefa = req.body;
tarefas.push(tarefa);
res.status(201).json(tarefa);
});
app.listen(8080, () => {
console.log("Servidor rodando na porta 8080");
});
Rust (Actix-web):
use actix_web::{web, App, HttpServer, HttpResponse};
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
#[derive(Serialize, Deserialize, Clone)]
struct Tarefa {
titulo: String,
concluida: bool,
}
struct AppState {
tarefas: Mutex<Vec<Tarefa>>,
}
async fn listar(data: web::Data<AppState>) -> HttpResponse {
let tarefas = data.tarefas.lock().unwrap();
HttpResponse::Ok().json(tarefas.clone())
}
async fn criar(
data: web::Data<AppState>,
tarefa: web::Json<Tarefa>,
) -> HttpResponse {
let mut tarefas = data.tarefas.lock().unwrap();
tarefas.push(tarefa.into_inner());
HttpResponse::Created().finish()
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let state = web::Data::new(AppState {
tarefas: Mutex::new(Vec::new()),
});
println!("Servidor rodando na porta 8080");
HttpServer::new(move || {
App::new()
.app_data(state.clone())
.route("/tarefas", web::get().to(listar))
.route("/tarefas", web::post().to(criar))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
O código Rust é mais verboso, mas oferece vantagens significativas: sem garbage collector (latência previsível), segurança de memória garantida em compilação e desempenho consideravelmente maior. Benchmarks consistentemente mostram que frameworks Rust como Actix-web superam Express/Fastify por uma margem expressiva.
Conclusão
A transição de JavaScript para Rust envolve aprender conceitos novos como ownership, lifetimes e tipagem estática rigorosa. No entanto, muitos padrões são familiares: closures, iteradores, async/await e um gerenciador de pacotes moderno.
Dicas para a transição:
- Se você usa TypeScript, está em vantagem – o hábito de pensar em tipos traduz diretamente para Rust.
- Ownership e borrowing – esse é o conceito mais novo. Dedique tempo para entender o borrow checker; as mensagens de erro do compilador Rust são excepcionalmente claras e educativas.
- Comece com
cargo– a experiência é superior ao ecossistema fragmentado do npm/webpack/jest. Tudo funciona de forma integrada. OptioneResultsão seus novos melhores amigos – substituem o caos denull/undefinede exceções silenciosas do JavaScript.- Use
serdepara JSON – a experiência é tão boa quantoJSON.parse/JSON.stringify, mas com segurança de tipos. - Explore WebAssembly – Rust compila para WASM, permitindo que você use Rust no frontend junto com JavaScript.
A curva de aprendizado compensa rapidamente: código mais seguro, mais rápido e com menos bugs em produção. Bem-vindo ao Rust!