Rust para Programadores JavaScript: Transição | Rust Brasil

Guia de transição de JavaScript/TypeScript para Rust: sintaxe, tipos, async e ferramentas comparados lado a lado.

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.

ConceitoJavaScriptRust
Imutávelconst x = 10;let x = 10;
Mutávellet x = 10;let mut x = 10;
Constante globalconst PI = 3.14;const PI: f64 = 3.14;
Sem tipo declaradolet x = 10;let x = 10; (tipo inferido)
Com tipo declaradolet 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.

ConceitoJavaScriptRust
Sem valorundefined / nullNone
Com valorvalor diretoSome(valor)
Checagemif (x != null)if let Some(v) = x
Acesso segurox?.prop (optional chaining)x.map(|v| v.prop)
Valor padrãox ?? "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.

ConceitoJavaScriptRust (Tokio)
Criar asyncasync function f()async fn f()
Aguardarawait promisefuture.await
Executar em paraleloPromise.all([...])tokio::join!(...)
Primeira a resolverPromise.race([...])tokio::select!(...)
TimeoutPromise.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

TarefaJavaScript (npm)Rust (Cargo)
Iniciar projetonpm initcargo new projeto
Arquivo de configpackage.jsonCargo.toml
Lock filepackage-lock.jsonCargo.lock
Instalar dependênciasnpm installcargo build
Adicionar pacotenpm install expresscargo add actix-web
Executarnode index.js / npm startcargo run
Testarnpm test (jest/vitest)cargo test
Formatarnpx prettier --write .cargo fmt
Lintnpx eslint .cargo clippy
Build produçãonpm run build (webpack, etc.)cargo build --release
Repositórionpmjs.comcrates.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.

TypeScriptRust
numberi32, f64, u32, etc.
stringString / &str
booleanbool
T[] / Array<T>Vec<T>
Record<K, V>HashMap<K, V>
T | nullOption<T>
interfacetrait
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:

  1. Se você usa TypeScript, está em vantagem – o hábito de pensar em tipos traduz diretamente para Rust.
  2. 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.
  3. Comece com cargo – a experiência é superior ao ecossistema fragmentado do npm/webpack/jest. Tudo funciona de forma integrada.
  4. Option e Result são seus novos melhores amigos – substituem o caos de null/undefined e exceções silenciosas do JavaScript.
  5. Use serde para JSON – a experiência é tão boa quanto JSON.parse/JSON.stringify, mas com segurança de tipos.
  6. 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!