Ir al contenido

Rust para desarrolladores de JavaScript y TypeScript

Si escribes JavaScript o TypeScript, ya sabes más Rust de lo que crees. Rust tiene closures que funcionan de forma muy parecida a las funciones flecha, async/await, métodos de iteradores con nombres conocidos como map, filter y find, desestructuración y una herramienta de línea de comandos (Cargo) que hace lo mismo que npm. Si usas TypeScript, también estás acostumbrado a un compilador que detecta errores antes de que tu código se ejecute. El compilador de Rust hace lo mismo, solo que es más estricto.

Entonces, ¿qué es lo realmente nuevo? Tres cosas. Rust no tiene recolector de basura (garbage collector): cada valor tiene un único dueño, y el compilador comprueba el ownership (propiedad) y el borrowing (préstamo) antes de que tu programa se ejecute. Rust no tiene undefined ni null: un valor que puede faltar tiene el tipo Option. Y Rust no tiene excepciones: una función que puede fallar devuelve un Result. La mayor parte de tu tiempo de aprendizaje se irá en estas tres ideas.

¿Por qué aprenden Rust los desarrolladores de JavaScript? Normalmente por uno de estos motivos: quieres un backend web o una herramienta de línea de comandos rápida que se distribuya como un único archivo, quieres acelerar un punto crítico del navegador con WebAssembly, quieres escribir un addon nativo para Node.js, o te pica la curiosidad por las herramientas que ya usas. SWC, Biome, Oxc, Rolldown, Rspack, Turbopack, Lightning CSS y Deno están escritos en Rust.

Esta página traduce a Rust lo que ya conoces de JavaScript y TypeScript, para que dediques tu tiempo a lo que de verdad es nuevo. Si prefieres una comparación general con C++, Go y Python, consulta Rust vs C++, Go y Python.

Conceptos del lenguaje: de JavaScript y TypeScript a Rust

Sección titulada «Conceptos del lenguaje: de JavaScript y TypeScript a Rust»

Usa esta tabla como un diccionario de bolsillo. La última columna enlaza con la lección de este sitio que explica la parte de Rust.

JavaScript / TypeScript Rust Para saber más
let y const let (no puede cambiar) y let mut (puede cambiar). Un objeto const de JS todavía se puede modificar por dentro; un valor let de Rust no Variables
number Eliges un tamaño: i32, i64, u32, usize, f64, … Los enteros y los decimales son tipos distintos Tipos de datos
bigint i128 / u128, o el crate num-bigint para cualquier tamaño Tipos de datos
string String (con dueño, puede crecer) y &str (una vista prestada de un texto) Strings
boolean bool Booleanos
Plantillas literales (template literals) format!("Hi {name}") y println!("Hi {name}") Salida en Rust
any, unknown No existe any. Usa genéricos, un enum o serde_json::Value para JSON de forma desconocida Genéricos, JSON con Serde
interface o type que describe la forma de un objeto struct Structs
interface que describe métodos trait Traits
Tipado estructural (misma forma = mismo tipo) Tipado nominal: dos structs con los mismos campos siguen siendo tipos distintos Structs
Tipos unión y uniones discriminadas enum, donde cada variante puede llevar sus propios datos Enums
Tipos literales de string como "small" o "large" Un enum con variantes simples: enum Size { Small, Large } Enums
undefined, null, propiedad opcional x?: T Option<T>: o bien Some(value) o bien None Option
?. y ?? ? (dentro de una función que devuelve Option), .map(...), .and_then(...), .unwrap_or(...) Option
Aserción de no nulo x! .unwrap() o .expect("motivo"), que detienen el programa si el valor es None Panic
throw, try/catch, Promises rechazadas Result<T, E> + el operador ? Result
class MyError extends Error Tu propio enum de error (a menudo con thiserror); anyhow en aplicaciones Errores personalizados, anyhow y thiserror
class con campos y métodos struct para los datos + un bloque impl para los métodos Structs
extends (herencia) No hay herencia. Usa composición (un struct dentro de otro) + traits con métodos por defecto Traits, Patrones de diseño
class Dog implements Animal impl Animal for Dog Traits
Un array de clases distintas que comparten una interfaz Vec<Box<dyn Animal>> (un trait object) Trait objects
Genéricos como T extends HasId Genéricos con trait bounds (restricciones): T: HasId Genéricos
Funciones flecha y callbacks Closures, descritas por los traits Fn, FnMut, FnOnce Closures, Fn, FnMut y FnOnce
this Un parámetro explícito self, &self o &mut self. Sin sorpresas sobre a qué apunta this Structs
Desestructuración const { x, y } = point Patrones: let Point { x, y } = point; (también en match y en los parámetros de funciones) Coincidencia de patrones
Spread { ...user, admin: true } Sintaxis de actualización de structs: User { admin: true, ..user } Structs
switch, o cadenas de if sobre typeof / kind match (el compilador comprueba que cubriste todos los casos) Match
=== ==, que compara valores (a través del trait PartialEq). Nunca hay conversión implícita de tipos Traits comunes
for...of for item in &items For en Rust
map, filter, reduce, find, some, every Métodos de iteradores: map, filter, fold, find, any, all y luego collect Iteradores
Generadores (function*) y Symbol.iterator Implementar el trait Iterator Iteradores personalizados
Promise<T> Un Future con Output = T. A diferencia de una Promise, no hace nada hasta que lo esperas con await Async/Await
async / await async fn y .await, más un runtime como tokio para ejecutarlos Async/Await, Async a fondo
Promise.all tokio::join!, o join_all del crate futures Async a fondo
setTimeout tokio::time::sleep en código async, std::thread::sleep en el resto Async/Await
Web Workers, worker_threads Hilos de verdad: std::thread::spawn, o rayon para bucles en paralelo Concurrencia
Módulos ES: import, export Módulos: mod, use y pub Módulos
JSON.parse / JSON.stringify serde_json con #[derive(Serialize, Deserialize)] JSON con Serde
console.log println!, y dbg! para depurar rápido. Usa {:?} con los tipos que derivan Debug Salida en Rust
readonly, as const, Object.freeze No hacen falta: todo es de solo lectura salvo que escribas mut Variables
Decoradores Atributos como #[derive(Debug)] y macros Macros
Number("abc") da NaN "abc".parse::<i32>() devuelve un Err que tienes que tratar Conversión de tipos, Conversiones
Recolector de basura Ownership y borrowing, comprobados al compilar Ownership, Borrowing
JavaScript Rust Para saber más
Array Vec<T> (todos los elementos tienen el mismo tipo) Vectores
Array de longitud fija, o una tupla de TS [string, number] [T; N] para una longitud fija, una tupla (String, i32) para tipos mezclados Arrays, Tuplas
Map, o un objeto usado como diccionario (Record<string, T>) HashMap<K, V> HashMap
Un mapa con las claves ordenadas BTreeMap<K, V> HashMap
Set HashSet<T> (o BTreeSet<T> para tenerlo ordenado) HashSet
Array usado como cola (push + shift) VecDeque<T> (push_back / pop_front, los dos rápidos) VecDeque y BinaryHeap
Array usado como pila (push + pop) Vec<T> con push / pop Vectores
Uint8Array, Buffer Vec<u8>, o un slice de bytes &[u8] Slices
WeakMap, WeakRef Weak<T> (de Rc o de Arc) Weak y Cow

Un detalle a tener en cuenta: un Map, un Set y un objeto normal de JavaScript recuerdan el orden en que insertaste las cosas. Un HashMap y un HashSet de Rust no. Si necesitas orden alfabético o numérico, usa BTreeMap; si necesitas el orden de inserción, el crate indexmap te da exactamente eso.

Con Cargo te vas a sentir como en casa. Es tu gestor de paquetes, tu herramienta de compilación, tu ejecutor de tests y tu ejecutor de scripts en uno, y todos los proyectos de Rust lo usan de la misma manera.

JavaScript Rust Notas
node + npm (o pnpm, yarn) cargo Compila, ejecuta, prueba, añade paquetes y publica. Consulta Cargo y crates
package.json Cargo.toml
package-lock.json Cargo.lock Se crea solo
node_modules/ No hay una carpeta por proyecto. Las descargas se guardan una sola vez en ~/.cargo, y lo compilado va a target/
npmjs.com crates.io La documentación de la API de cada crate publicado está en docs.rs
npm install zod cargo add serde
npm init cargo new my_app (cargo new --lib para una biblioteca)
npm run dev / build / test cargo run / cargo build / cargo test Añade --release para una compilación optimizada; las compilaciones debug son mucho más lentas
"scripts" en package.json No hay sección de scripts. Usa alias de cargo en .cargo/config.toml, o un ejecutor de tareas como just
npx some-tool cargo install some-tool, y luego lo ejecutas cargo binstall descarga binarios ya compilados en lugar de compilarlos
nvm y .nvmrc rustup y un archivo rust-toolchain.toml Consulta Herramientas de Rust
Workspaces de npm / pnpm (monorepo) Un workspace de Cargo Un Cargo.toml en la raíz enumera los crates miembros
tsc rustc, al que cargo llama por ti
tsc --noEmit cargo check Comprueba los tipos sin compilar del todo; mucho más rápido
ESLint cargo clippy Cientos de lints útiles, oficiales
Prettier cargo fmt Un único estilo estándar para todos
nodemon, tsx watch cargo watch o bacon Se instalan con cargo install
Jest / Vitest / Mocha #[test] y cargo test, incluidos de serie (cargo nextest es un ejecutor más rápido muy popular) Consulta Pruebas en Rust
Snapshots de Jest insta Consulta Pruebas avanzadas
jest.mock() mockall, o implementaciones falsas escritas a mano que implementan tu trait
fast-check proptest Pruebas basadas en propiedades
c8 / Istanbul (cobertura) cargo llvm-cov
JSDoc / TypeDoc Comentarios de documentación /// en Markdown + cargo doc Los ejemplos de código de la documentación se ejecutan como tests. Consulta Documentación en Rust
VS Code + el servidor de TypeScript VS Code + rust-analyzer, o JetBrains RustRover rust-analyzer ofrece autocompletado, ir a la definición y errores en línea
npm publish cargo publish

Aquí tienes un Cargo.toml pequeño, para que lo compares con un 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"] }

Una versión como "1" significa lo mismo que "^1" en package.json: cualquier versión 1.x compatible. Las features son partes opcionales de un crate que activas tú; consulta Features de Cargo y cfg.

Bibliotecas y frameworks: “¿Qué usamos en lugar de…?”

Sección titulada «Bibliotecas y frameworks: “¿Qué usamos en lugar de…?”»

Es lo primero que pregunta cualquier desarrollador de JavaScript. “Usamos Express y Prisma. ¿Cuál es la versión de Rust?” Aquí tienes los equivalentes más cercanos. Igual que npm, crates.io tiene muchas opciones para casi cualquier tarea; estas son las populares y bien mantenidas.

JavaScript Rust Notas
Express, Fastify, Koa axum (construimos una API en Proyecto: API web con Axum), actix-web, Rocket axum y actix-web se usan mucho en producción
Middleware de Express Capas de tower y el crate tower-http (CORS, compresión, trazas, timeouts) axum usa tower directamente
fetch, axios reqwest Asíncrono por defecto, con un cliente bloqueante opcional. Consulta Redes en Rust
ws, Socket.IO WebSockets en axum (la feature ws), tokio-tungstenite o socketioxide (un servidor de Socket.IO)
Apollo Server (GraphQL) async-graphql
gRPC (@grpc/grpc-js) tonic (con prost para protobuf)
Generadores de Swagger / OpenAPI utoipa
jsonwebtoken jsonwebtoken Mismo nombre, misma idea
bcrypt argon2 o bcrypt
JavaScript Rust Notas
Prisma, TypeORM SeaORM (asíncrono, con entidades y migraciones) o Diesel (consultas comprobadas por tipos al compilar) SeaORM es el que más se parece a Prisma y TypeORM
Knex, pg, postgres.js sqlx: escribes SQL normal, y la macro query! lo comprueba contra tu base de datos real al compilar Asíncrono, muy popular, no es un ORM
better-sqlite3 rusqlite, o sqlx con SQLite
Mongoose, el driver de MongoDB mongodb (el driver oficial) No hay esquemas al estilo Mongoose; usas structs de serde
ioredis redis

Bases de datos en Rust muestra sqlx, Diesel y SeaORM con código que funciona.

JavaScript Rust Notas
JSON.parse / JSON.stringify serde + serde_json El estándar; también sirve para TOML, YAML y más. Consulta JSON con Serde
Zod, Yup Los tipos de tu struct + serde hacen casi todo el trabajo; validator o garde para reglas como “al menos 8 caracteres” Si el JSON no encaja con el struct, la lectura falla con un error
Lodash Sobre todo los iteradores de la biblioteca estándar; itertools para extras como unique, sorted_by y chunk_by
date-fns, Day.js, Luxon chrono o jiff Consulta Fechas y horas
dotenv dotenvy Consulta Entorno y procesos
Commander, yargs clap Consulta Proyecto: CLI con clap
chalk colored u owo-colors
winston, pino tracing (+ tracing-subscriber), o log + env_logger Consulta Logging en Rust
uuid uuid
EventEmitter Canales (std::sync::mpsc, o tokio::sync::broadcast para muchos oyentes) Consulta Canales
BullMQ (colas de trabajos) tokio::spawn para tareas dentro del proceso; apalis para colas de trabajos persistentes
Expresiones regulares regex Sin lookahead ni referencias hacia atrás, a propósito. Consulta Expresiones regulares
JavaScript Rust Notas
Acelerar código pesado del navegador Compilar Rust a WebAssembly con wasm-bindgen y wasm-pack Consulta Rust y WebAssembly y la sección de más abajo
React, Vue, Svelte Leptos, Yew, Dioxus (todos compilan a WebAssembly) Todos antes de la versión 1.0; sus APIs aún cambian
Electron Tauri: tu frontend web + un backend en Rust, con apps mucho más pequeñas Tauri 2 es estable. Consulta Apps con interfaz gráfica
Addons nativos de Node.js (C++ con node-gyp) napi + napi-derive (napi-rs), o neon Publica binarios ya compilados en npm; no hace falta C++
Juegos con Phaser o Three.js Bevy, macroquad Bevy aún no llega a la 1.0 y cambia su API entre versiones. Consulta Desarrollo de videojuegos

Lado a lado: una interfaz se convierte en struct

Sección titulada «Lado a lado: una interfaz se convierte en struct»

Aquí tienes un objeto de TypeScript con una interfaz, una función que hace de método y una copia con 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);

En Rust, la forma se convierte en un struct, y la función se convierte en un método dentro de un bloque impl:

#[derive(Debug, Clone)]
struct User {
name: String,
age: u32,
admin: bool,
}
impl User {
// `&self` significa "este método lee el usuario, no lo modifica".
fn greeting(&self) -> String {
format!("Hola, soy {} ({})", self.name, self.age)
}
}
fn main() {
let ada = User { name: "Ada".to_string(), age: 36, admin: false };
// Como { ...ada, admin: true } en JavaScript.
let promoted = User { admin: true, ..ada.clone() };
println!("{}", ada.greeting());
println!("{:?}", promoted);
}

Salida:

Hola, soy Ada (36)
User { name: "Ada", age: 36, admin: true }
▶ Pruébalo tú mismo

Hay varias diferencias. Cada campo tiene un tipo exacto: age es un u32 (un número entero que no puede ser negativo), no un number genérico. #[derive(Debug)] es lo que te permite imprimir el struct entero con {:?}, como console.log(obj). ¿Y por qué .clone()? El spread ..ada movería el string name fuera de ada, y ya no podríamos usar ada. Clonar primero deja ada intacto. Eso es el ownership en acción, y lo vas a ver muy a menudo.

Lado a lado: una unión discriminada se convierte en enum

Sección titulada «Lado a lado: una unión discriminada se convierte en enum»

A los desarrolladores de TypeScript les encantan las uniones discriminadas: un campo kind te dice qué forma tienes, y switch estrecha el 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));

Rust trae esto de serie, y es una de las mejores características del lenguaje. Cada variante de un enum lleva sus propios datos, y match los separa:

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));
}
}

Salida:

3.14
6.00
▶ Pruébalo tú mismo

No hace falta ningún string kind: la propia variante es la etiqueta. Y match tiene que cubrir todas las variantes. Si más adelante añades Triangle, cada match que se olvide de ella se convierte en un error de compilación, que es como el truco de exhaustividad con never de TypeScript, pero siempre activado. Tienes más sobre esto en Coincidencia de patrones.

Lado a lado: undefined y null se convierten en Option

Sección titulada «Lado a lado: undefined y null se convierten en Option»

En TypeScript, los valores opcionales aparecen como undefined, null o una propiedad opcional, y usas ?. y ?? para tratarlos:

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

En Rust, un valor que puede faltar tiene el tipo Option<T>. Es o bien Some(value) o bien None, y no puedes usarlo como un valor normal hasta que hayas tratado el 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 una función que devuelve Option, `?` funciona como `?.`:
// si el valor es None, se detiene aquí y devuelve 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() ?? "sin email"
let email = find_user(id)
.and_then(|user| user.email)
.map(|email| email.to_uppercase())
.unwrap_or("sin email".to_string());
println!("{id}: {email}");
}
println!("{:?}", email_domain(1));
println!("{:?}", email_domain(2));
if let Some(user) = find_user(2) {
println!("encontrado: {}", user.name);
}
}

Salida:

2: sin email
3: sin email
Some("example.com")
None
encontrado: Grace
▶ Pruébalo tú mismo

En Rust no existe el “cannot read properties of undefined”, porque no hay ningún undefined del que leer. El compilador simplemente no te deja llamar a .to_uppercase() sobre un Option<String>. Y no tienes que recordar la diferencia entre null y undefined: solo existe None. Aprende los métodos auxiliares en la página de Option, y las formas cortas en If Let / While Let en Rust.

Lado a lado: try/catch se convierte en Result y ?

Sección titulada «Lado a lado: try/catch se convierte en Result y ?»

En JavaScript, una función lanza un error con throw, y tú lo capturas en algún punto más arriba de la pila de llamadas. Nada en la firma de la función te dice que puede lanzarlo:

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

En Rust, una función que puede fallar devuelve un Result: o bien Ok(value) o bien Err(error). El operador ? significa “si esto falló, devuelve el error a quien me llamó, ahora mismo”. Hace el mismo trabajo que dejar que una excepción suba por la pila, pero lo ves en el código:

fn parse_age(input: &str) -> Result<u32, String> {
let age: u32 = input
.parse()
.map_err(|_| format!("'{input}' no es un número entero"))?;
if age > 150 {
return Err(format!("{age} es una edad demasiado alta"));
}
Ok(age)
}
fn total_age(a: &str, b: &str) -> Result<u32, String> {
// Cada `?` pasa el error a quien nos llamó, como lo haría una excepción.
let first = parse_age(a)?;
let second = parse_age(b)?;
Ok(first + second)
}
fn main() {
for (a, b) in [("36", "41"), ("36", "abc"), ("36", "200")] {
match total_age(a, b) {
Ok(total) => println!("Total: {total}"),
Err(e) => println!("Error: {e}"),
}
}
}

Salida:

Total: 77
Error: 'abc' no es un número entero
Error: 200 es una edad demasiado alta
▶ Pruébalo tú mismo

La firma -> Result<u32, String> le dice a quien llama “esto puede fallar”, y el compilador no le deja ignorarlo. Lo mismo pasa con el código async: donde en JavaScript tendrías una Promise rechazada, una función async de Rust devuelve un Result, y escribes .await?. Para tus propios tipos de error, consulta Errores personalizados. En aplicaciones, el crate anyhow te da algo parecido a “capturar cualquier error” (consulta anyhow y thiserror).

Lado a lado: los métodos de arrays se convierten en iteradores

Sección titulada «Lado a lado: los métodos de arrays se convierten en iteradores»

Los métodos de arrays que usas cada día tienen equivalentes directos en 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)}`);

La versión en Rust usa iteradores y closures. La sintaxis de un closure es |o| o.paid en lugar 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, y luego collect() convierte el resultado en un Vec.
let paid_names: Vec<&str> = orders
.iter()
.filter(|o| o.paid)
.map(|o| o.customer.as_str())
.collect();
// reduce con un valor inicial se llama fold.
let revenue = orders
.iter()
.filter(|o| o.paid)
.fold(0, |sum, o| sum + o.total);
// find devuelve un Option, porque puede que nada coincida.
let first_big = orders.iter().find(|o| o.total > 150);
println!("{}", paid_names.join(", "));
println!("Ingresos: {revenue}");
match first_big {
Some(o) => println!("Primer pedido grande: {}", o.customer),
None => println!("Primer pedido grande: ninguno"),
}
// some() se llama any().
println!("¿Alguno sin pagar? {}", orders.iter().any(|o| !o.paid));
}

Salida:

Ada, Linus
Ingresos: 320
Primer pedido grande: Linus
¿Alguno sin pagar? true
▶ Pruébalo tú mismo

Hay una gran diferencia con JavaScript. En JS, cada método de array construye un array nuevo completo en el momento. Los iteradores de Rust son perezosos (lazy): filter y map no hacen nada por sí solos, y el trabajo se hace en una sola pasada cuando llamas a algo como collect(), sum(), fold() o usas un bucle for. No se crean arrays temporales por el camino. Solo para la suma, también podrías escribir .map(|o| o.total).sum::<u32>(). La ordenación está en Vec (sort, sort_by), no en los iteradores; consulta Ordenar y buscar.

Lado a lado: las Promises se convierten en futures + tokio

Sección titulada «Lado a lado: las Promises se convierten en futures + tokio»

Aquí tienes algo de JavaScript async. Fíjate en cuándo se imprime "fetching 1":

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" enseguida
console.log("promise created");
console.log(await promise);
const [a, b] = await Promise.all([fetchUser(2), fetchUser(3)]);
console.log(`${a}, ${b}`);

En JavaScript, llamar a una función async la pone en marcha inmediatamente. Rust es distinto: llamar a una async fn solo crea un future, una descripción del trabajo. No se ejecuta nada hasta que haces .await. Y Rust no trae un event loop incorporado, así que añades un runtime: tokio es el más popular.

use std::time::Duration;
async fn fetch_user(id: u32) -> String {
println!("buscando {id}");
tokio::time::sleep(Duration::from_millis(100)).await;
format!("usuario {id}")
}
#[tokio::main]
async fn main() {
// Crear un future NO lo pone en marcha. Todavía no se imprime nada.
let future = fetch_user(1);
println!("future creado");
println!("{}", future.await);
// Como Promise.all: ejecuta los dos a la vez y espera a ambos.
let (a, b) = tokio::join!(fetch_user(2), fetch_user(3));
println!("{a}, {b}");
}

Salida:

future creado
buscando 1
usuario 1
buscando 2
buscando 3
usuario 2, usuario 3
▶ Pruébalo tú mismo

¿Ves el orden? En JavaScript, “fetching 1” saldría antes que “promise created”. En Rust, “buscando 1” sale después de “future creado”, porque el future solo empieza con .await. #[tokio::main] prepara el runtime (tu event loop) y ejecuta main en él. tokio::join! espera a los dos futures a la vez, como Promise.all; tokio::spawn lanza una tarea en segundo plano, que es lo más parecido a llamar a una función async sin esperarla. Las lecciones Async/Await y Async a fondo explican los detalles.

Rust en tu proyecto de JavaScript: WebAssembly y addons de Node

Sección titulada «Rust en tu proyecto de JavaScript: WebAssembly y addons de Node»

No tienes que reescribir nada para empezar a usar Rust. Hay dos formas habituales de meterlo en un proyecto de JavaScript:

WebAssembly en el navegador. Marca las funciones de Rust con #[wasm_bindgen], compila con wasm-pack e importa el resultado como cualquier módulo ES. wasm-pack incluso escribe un archivo .d.ts, así que TypeScript conoce los tipos de tus funciones:

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(); // descarga y arranca el archivo .wasm
console.log(slugify("Hello Rust World")); // "hello-rust-world"

La lección Rust y WebAssembly te guía por toda la configuración y explica qué valores cruzan la frontera sin coste. Un consejo: Wasm compensa para cálculos pesados (procesar imágenes, analizar texto, simulaciones), no para funciones pequeñas que llamas miles de veces, porque cada llamada entre JS y Wasm tiene un pequeño coste.

Addons nativos de Node.js. Con napi-rs (los crates napi y napi-derive) marcas una función de Rust con #[napi], y se convierte en una función que puedes cargar con require o import desde Node. Muchos paquetes populares de npm publican así binarios de Rust ya compilados, de modo que los usuarios nunca necesitan un compilador de Rust.

Lo que te va a hacer tropezar si vienes de JavaScript

Sección titulada «Lo que te va a hacer tropezar si vienes de JavaScript»

Todo el que llega desde JavaScript se encuentra con los mismos baches. Conocerlos de antemano ayuda.

Pasar un valor a una función lo mueve. En JavaScript, pasar un array a una función pasa una referencia, y puedes seguir usando el array después. En Rust, pasar un Vec o un String por valor mueve el ownership a la función, y la variable original ya no se puede usar:

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

El compilador te dice que names se movió en la línea 9 (“value moved here”) y que después intentas usarlo en la línea 10. La solución habitual es tomarlo prestado: haz que la función reciba names: &[String] y llámala con print_all(&names). Lee primero Ownership y Borrowing; ahí se explica esta regla, y te la vas a encontrar todos los días.

Los números tienen tipos, y no se mezclan. JavaScript tiene un solo number. En Rust, 3 es un entero y 2.5 es un decimal, y no puedes combinarlos sin una conversión:

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

El mensaje dice que no se puede multiplicar un entero por un decimal. Escribe items as f64 * price, o haz que items sea un decimal desde el principio (3.0). Ten en cuenta también que la división entera descarta el resto: 7 / 2 es 3, no 3.5. Consulta Tipos de datos y Conversión de tipos.

Algunos más con los que te vas a encontrar:

  • Los futures son perezosos y necesitan un runtime. Como viste antes, no pasa nada hasta el .await, y necesitas tokio (u otro runtime) para ejecutar código async. Si olvidas el .await, obtienes un aviso del compilador, no una tarea en marcha.
  • Los strings no se pueden indexar. name[0] no compila, porque un String de Rust es UTF-8 y un carácter puede ocupar varios bytes. Usa name.chars().next() o .chars().nth(i). Y aprende pronto la diferencia entre String y &str: las funciones suelen recibir &str y los structs suelen guardar String. Consulta Strings en Rust.
  • No hay tipado estructural ni any. En TypeScript, cualquier objeto con los campos adecuados encaja en una interfaz. En Rust, una función que recibe un User solo acepta un User. Para comportamiento compartido, usa traits. Para JSON cuya forma no conoces, usa serde_json::Value.
  • Los grafos de objetos compartidos y mutables son difíciles. En JavaScript es normal pasar un objeto de un lado a otro y dejar que todos lo modifiquen, o que los objetos apunten a su padre. El borrow checker (comprobador de préstamos) permite muchos lectores o un solo escritor a la vez, nunca las dos cosas. Patrones así necesitan punteros inteligentes (Rc<RefCell<T>>, Arc<Mutex<T>>) o, mejor, un diseño distinto. Cuenta con unas semanas frustrantes. Luego se vuelve mucho más fácil.
  • HashMap no tiene orden. Los Map y los objetos de JavaScript mantienen el orden de inserción, y el código a menudo depende de ello sin darse cuenta. Un HashMap de Rust no. Ordena antes de imprimir, o usa BTreeMap o el crate indexmap.
  • No hay excepciones. Los errores son valores, y vas a escribir ? muchas veces. Llamar a .unwrap() está bien mientras aprendes, pero es la versión de Rust del ! de TypeScript: “confía en mí, está ahí”. Si te equivocas, el programa entra en pánico.
  • Tiempos de compilación. No hay recarga instantánea como con node app.js. Una primera compilación con muchos crates tarda un rato; las siguientes son incrementales y más rápidas. Usa cargo check mientras editas, porque se salta la generación de código.

Una ruta de aprendizaje para desarrolladores de JavaScript

Sección titulada «Una ruta de aprendizaje para desarrolladores de JavaScript»

No necesitas leer este sitio en orden. Si vienes de JavaScript o TypeScript, esta ruta se salta lo que ya sabes y dedica el tiempo a lo que Rust hace distinto:

  1. Empezar con Rust y Herramientas de Rust: instala Rust, configura VS Code con rust-analyzer y ejecuta cargo new.
  2. Variables y Tipos de datos: léelas con atención. Los tipos numéricos son nuevos para ti.
  3. Ownership, luego Borrowing y luego Slices: las páginas más importantes para ti. Tómate tu tiempo.
  4. Strings: String frente a &str.
  5. Structs y Enums, luego Match y Coincidencia de patrones: tus interfaces y tus uniones discriminadas.
  6. Option y Result, luego Errores personalizados.
  7. Closures e Iteradores: tus métodos de arrays.
  8. Vectores, HashMap y HashSet.
  9. Traits, Genéricos y Traits comunes.
  10. Módulos, Cargo y crates y Pruebas.
  11. Lifetimes y Punteros inteligentes.
  12. Async/Await y Async a fondo.
  13. JSON con Serde, y después construye el proyecto de API web o prueba WebAssembly.

Los dos ejercicios te dan un fragmento de JavaScript o TypeScript. Pásalo a Rust.

Este código de JavaScript cuenta cuántas veces aparece cada palabra y luego imprime las palabras de la más frecuente a la menos frecuente (los empates, en orden alfabético). Pásalo a Rust. Pista: usa un HashMap<&str, u32> y *counts.entry(word).or_insert(0) += 1. Para ordenar, recoge el mapa en un Vec de pares (palabra, cantidad) y usa sort_by. El texto se queda en inglés porque el orden alfabético de la salida depende de las letras.

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 solución
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 [...counts]: convierte el mapa en una lista de pares (palabra, cantidad).
let mut sorted: Vec<(&str, u32)> = counts.into_iter().collect();
// Primero las más frecuentes; los empates, en orden alfabético.
sorted.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (word, n) in sorted {
println!("{word}: {n}");
}
}

Salida:

the: 3
and: 2
bat: 1
cat: 1
hat: 1
▶ Pruébalo tú mismo

entry(word).or_insert(0) hace la parte de counts.get(word) ?? 0 y el set en un solo paso: te da una referencia mutable a la cantidad, y la crea con 0 si no existe. Aquí ordenar es imprescindible, no un simple detalle: sin ordenar, un HashMap imprime en un orden distinto en cada ejecución.

2. Pasa a Rust unas opciones con valores por defecto

Sección titulada «2. Pasa a Rust unas opciones con valores por defecto»

Esta función de TypeScript recibe un objeto de opciones en el que todos los campos son opcionales, y rellena los valores por defecto con ??. Pásala a Rust. Pista: crea un struct con campos Option y añade #[derive(Default)]. Así, ServerOptions { port: Some(3000), ..Default::default() } funciona como pasar { port: 3000 }. Para un bool, el valor por defecto es 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 solución
#[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 { "detallado" } 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()
})
);
}

Salida:

localhost:8080 (silencioso)
localhost:3000 (silencioso)
example.com:8080 (detallado)
▶ Pruébalo tú mismo

unwrap_or es tu ??. as_deref() convierte un Option<String> en un Option<&str>, para que pueda recurrir al texto "localhost" sin copiar nada. Y ..Default::default() es la forma de Rust de decir “todos los campos que no he mencionado toman su valor por defecto”, que es justo lo que hace en JavaScript no poner una propiedad. Usar un u16 para el puerto también significa que un puerto como 70000 ni siquiera se puede guardar por error.

  • Mucho de JavaScript y TypeScript te sirve en Rust: los closures, async/await, la desestructuración y los métodos al estilo de los arrays, como map, filter y find, tienen equivalentes muy parecidos.
  • Los grandes cambios son el ownership en lugar de un recolector de basura, Option en lugar de undefined y null, y Result + ? en lugar de throw y try/catch.
  • Las interfaces de TypeScript se convierten en structs (para los datos) o en traits (para el comportamiento), y las uniones discriminadas en enums con match.
  • Los futures de Rust son perezosos y necesitan un runtime como tokio; tokio::join! hace el papel de Promise.all.
  • Cargo sustituye a npm, a package.json y a tus scripts; clippy y rustfmt sustituyen a ESLint y Prettier; los tests vienen incluidos.
  • En lugar de Express, mira axum o actix-web; en lugar de fetch, reqwest; en lugar de Prisma, SeaORM o Diesel; y en lugar de Electron, Tauri. También puedes llevar Rust a un proyecto de JS con WebAssembly o napi-rs.

Comprueba lo aprendido

1. ¿Qué usa Rust en lugar de undefined y null?

2. En JavaScript, llamar a una función async la pone en marcha enseguida. ¿Qué pasa si llamas a una async fn en Rust sin .await?

3. Tu código de TypeScript usa una unión discriminada con un campo kind. ¿Qué característica de Rust se le parece más?

4. El backend de tu equipo usa Express. ¿Qué crate de Rust se le parece más?

Puntuación: 0 / 4

  • El lenguaje de programación Rust: el libro oficial, en su traducción de la comunidad. Los capítulos 4 (ownership), 6 (enums y coincidencia de patrones) y 9 (errores) son los más importantes si vienes de JavaScript.
  • Tokio tutorial (en inglés): la guía oficial de Rust async con tokio, desde tu primera tarea hasta los canales y la E/S.
  • The wasm-bindgen Guide (en inglés): cómo se comunican Rust y JavaScript en WebAssembly, con muchos ejemplos.
  • Compiling from Rust to WebAssembly (MDN) (en inglés): un tutorial paso a paso de MDN Web Docs, que incluye cómo publicar en npm.
  • napi-rs (en inglés): la documentación para escribir addons de Node.js en Rust.