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 |
Colecciones
Sección titulada «Colecciones»| 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.
Herramientas: de npm a cargo
Sección titulada «Herramientas: de npm a cargo»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.
Web y redes
Sección titulada «Web y redes»| 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 |
Acceso a datos
Sección titulada «Acceso a datos»| 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.
Piezas del día a día
Sección titulada «Piezas del día a día»| 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 |
Frontend, escritorio y Node.js
Sección titulada «Frontend, escritorio y Node.js»| 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 }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.146.00No 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 === 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 { 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 email3: sin emailSome("example.com")Noneencontrado: GraceEn 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: 77Error: 'abc' no es un número enteroError: 200 es una edad demasiado altaLa 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, LinusIngresos: 320Primer pedido grande: Linus¿Alguno sin pagar? trueHay 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" enseguidaconsole.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 creadobuscando 1usuario 1buscando 2buscando 3usuario 2, usuario 3¿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 .wasmconsole.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 here10 | println!("{} names", names.len()); | ^^^^^ value borrowed here after moveEl 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 unStringde Rust es UTF-8 y un carácter puede ocupar varios bytes. Usaname.chars().next()o.chars().nth(i). Y aprende pronto la diferencia entreStringy&str: las funciones suelen recibir&stry los structs suelen guardarString. 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 unUsersolo acepta unUser. Para comportamiento compartido, usa traits. Para JSON cuya forma no conoces, usaserde_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. HashMapno tiene orden. LosMapy los objetos de JavaScript mantienen el orden de inserción, y el código a menudo depende de ello sin darse cuenta. UnHashMapde Rust no. Ordena antes de imprimir, o usaBTreeMapo el crateindexmap.- 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. Usacargo checkmientras 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:
- Empezar con Rust y Herramientas de Rust: instala Rust, configura VS Code con rust-analyzer y ejecuta
cargo new. - Variables y Tipos de datos: léelas con atención. Los tipos numéricos son nuevos para ti.
- Ownership, luego Borrowing y luego Slices: las páginas más importantes para ti. Tómate tu tiempo.
- Strings:
Stringfrente a&str. - Structs y Enums, luego Match y Coincidencia de patrones: tus interfaces y tus uniones discriminadas.
- Option y Result, luego Errores personalizados.
- Closures e Iteradores: tus métodos de arrays.
- Vectores, HashMap y HashSet.
- Traits, Genéricos y Traits comunes.
- Módulos, Cargo y crates y Pruebas.
- Lifetimes y Punteros inteligentes.
- Async/Await y Async a fondo.
- JSON con Serde, y después construye el proyecto de API web o prueba WebAssembly.
Práctica
Sección titulada «Práctica»Los dos ejercicios te dan un fragmento de JavaScript o TypeScript. Pásalo a Rust.
1. Cuenta palabras con un Map
Sección titulada «1. Cuenta palabras con un Map»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: 3and: 2bat: 1cat: 1hat: 1entry(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)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.
Resumen
Sección titulada «Resumen»- 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,filteryfind, tienen equivalentes muy parecidos. - Los grandes cambios son el ownership en lugar de un recolector de basura,
Optionen lugar deundefinedynull, yResult+?en lugar dethrowytry/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 dePromise.all. - Cargo sustituye a npm, a
package.jsony 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 que aprendiste
Sección titulada «Comprueba lo que aprendiste»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
Para leer más
Sección titulada «Para leer más»- 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.