Rust для разработчиков на JavaScript и TypeScript
Если ты пишешь на JavaScript или TypeScript, ты знаешь о Rust больше, чем думаешь. В Rust есть замыкания, которые работают почти как стрелочные функции, async/await, методы итераторов со знакомыми именами вроде map, filter и find, деструктуризация и один консольный инструмент (Cargo), который делает то же, что npm. Если ты пишешь на TypeScript, ты уже привык к компилятору, который ловит ошибки до запуска кода. Компилятор Rust делает то же самое, только строже.
Так что же на самом деле нового? Три вещи. В Rust нет сборщика мусора: у каждого значения один владелец, а компилятор проверяет владение (ownership) и заимствование (borrowing) до запуска программы. В Rust нет undefined и null: значение, которого может не быть, имеет тип Option. И в Rust нет исключений: функция, которая может завершиться неудачей, возвращает Result. Большая часть времени на обучение уйдёт на эти три идеи.
Зачем разработчики на JavaScript учат Rust? Обычно по одной из этих причин: нужен быстрый веб-бэкенд или консольная утилита, которая поставляется одним файлом; нужно ускорить узкое место в браузере с помощью WebAssembly; нужно написать нативный аддон для Node.js; или просто интересно, как устроены инструменты, которыми ты уже пользуешься. SWC, Biome, Oxc, Rolldown, Rspack, Turbopack, Lightning CSS и Deno написаны на Rust.
Эта страница сопоставляет то, что ты знаешь по JavaScript и TypeScript, с Rust, чтобы ты мог потратить время на то, что действительно ново. Если тебе нужно общее сравнение с C++, Go и Python, загляни в урок Rust против C++, Go и Python.
Понятия языка: из JavaScript и TypeScript в Rust
Заголовок раздела «Понятия языка: из JavaScript и TypeScript в Rust»Пользуйся этой таблицей как разговорником. В последнем столбце — ссылка на урок этого сайта, который объясняет сторону Rust.
| JavaScript / TypeScript | Rust | Подробнее |
|---|---|---|
let и const |
let (менять нельзя) и let mut (можно). Объект в const в JS всё ещё можно менять изнутри; значение в let в Rust — нельзя |
Переменные |
number |
Размер выбираешь ты: i32, i64, u32, usize, f64, … Целые и дробные числа — разные типы |
Типы данных |
bigint |
i128 / u128 или крейт num-bigint для чисел любого размера |
Типы данных |
string |
String (владеющая, растущая строка) и &str (заимствованный взгляд на текст) |
Строки |
boolean |
bool |
Логический тип |
| Шаблонные строки | format!("Hi {name}") и println!("Hi {name}") |
Вывод на экран |
any, unknown |
any нет. Используй обобщения, перечисление или serde_json::Value для JSON неизвестной формы |
Обобщения, JSON и Serde |
interface или type, описывающие форму объекта |
struct |
Структуры |
interface, описывающий методы |
trait |
Трейты |
| Структурная типизация (одинаковая форма = один тип) | Номинальная типизация: две структуры с одинаковыми полями — всё равно разные типы | Структуры |
| Объединения типов и размеченные объединения (discriminated unions) | enum, у которого каждый вариант может нести свои данные |
Перечисления |
Строковые литеральные типы вроде "small" или "large" |
enum с простыми вариантами: enum Size { Small, Large } |
Перечисления |
undefined, null, необязательное свойство x?: T |
Option<T>: либо Some(value), либо None |
Option |
?. и ?? |
? (внутри функции, которая возвращает Option), .map(...), .and_then(...), .unwrap_or(...) |
Option |
Утверждение non-null x! |
.unwrap() или .expect("причина"), которые останавливают программу, если значение — None |
Паника |
throw, try/catch, отклонённые Promise |
Result<T, E> + оператор ? |
Result |
class MyError extends Error |
Своё перечисление ошибок (часто с thiserror); anyhow в приложениях |
Свои типы ошибок, anyhow и thiserror |
class с полями и методами |
struct для данных + блок impl для методов |
Структуры |
extends (наследование) |
Наследования нет. Используй композицию (структура внутри структуры) + трейты с методами по умолчанию | Трейты, Паттерны проектирования |
class Dog implements Animal |
impl Animal for Dog |
Трейты |
| Массив объектов разных классов с общим интерфейсом | Vec<Box<dyn Animal>> (трейт-объект) |
Трейт-объекты |
Обобщения вроде T extends HasId |
Обобщения с ограничениями трейтов: T: HasId |
Обобщения |
| Стрелочные функции и колбэки | Замыкания, описываемые трейтами Fn, FnMut, FnOnce |
Замыкания, Fn, FnMut и FnOnce |
this |
Явный параметр self, &self или &mut self. Никаких сюрпризов с тем, куда указывает this |
Структуры |
Деструктуризация const { x, y } = point |
Образцы: let Point { x, y } = point; (также в match и в параметрах функций) |
Сопоставление с образцом |
Spread { ...user, admin: true } |
Синтаксис обновления структуры: User { admin: true, ..user } |
Структуры |
switch или цепочки if по typeof / kind |
match (компилятор проверяет, что ты учёл все случаи) |
match |
=== |
==, который сравнивает значения (через трейт PartialEq). Никакого неявного приведения типов, никогда |
Популярные трейты |
for...of |
for item in &items |
Цикл for |
map, filter, reduce, find, some, every |
Методы итераторов: map, filter, fold, find, any, all, затем collect |
Итераторы |
Генераторы (function*) и Symbol.iterator |
Реализуй трейт Iterator |
Свои итераторы |
Promise<T> |
Future с Output = T. В отличие от Promise, ничего не делает, пока его не дождутся через await |
Async/Await |
async / await |
async fn и .await плюс среда выполнения вроде tokio, чтобы их запускать |
Async/Await, Async подробно |
Promise.all |
tokio::join! или join_all из крейта futures |
Async подробно |
setTimeout |
tokio::time::sleep в асинхронном коде, иначе std::thread::sleep |
Async/Await |
Web Workers, worker_threads |
Настоящие потоки: std::thread::spawn или rayon для параллельных циклов |
Многопоточность |
ES-модули: import, export |
Модули: mod, use и pub |
Модули |
JSON.parse / JSON.stringify |
serde_json с #[derive(Serialize, Deserialize)] |
JSON и Serde |
console.log |
println!, а для быстрой отладки — dbg!. Для типов с derive Debug используй {:?} |
Вывод на экран |
readonly, as const, Object.freeze |
Не нужны: всё и так только для чтения, пока не напишешь mut |
Переменные |
| Декораторы | Атрибуты вроде #[derive(Debug)] и макросы |
Макросы |
Number("abc") даёт NaN |
"abc".parse::<i32>() возвращает Err, который нужно обработать |
Приведение типов, Преобразования |
| Сборщик мусора | Владение и заимствование, проверяемые при компиляции | Владение, Заимствование |
Коллекции
Заголовок раздела «Коллекции»| JavaScript | Rust | Подробнее |
|---|---|---|
Array |
Vec<T> (все элементы одного типа) |
Векторы |
Массив фиксированной длины или кортеж TS [string, number] |
[T; N] для фиксированной длины, кортеж (String, i32) для разных типов |
Массивы, Кортежи |
Map или объект в роли словаря (Record<string, T>) |
HashMap<K, V> |
HashMap |
| Словарь с отсортированными ключами | BTreeMap<K, V> |
HashMap |
Set |
HashSet<T> (или BTreeSet<T> для отсортированного порядка) |
HashSet |
Массив в роли очереди (push + shift) |
VecDeque<T> (push_back / pop_front, обе операции быстрые) |
VecDeque и BinaryHeap |
Массив в роли стека (push + pop) |
Vec<T> с push / pop |
Векторы |
Uint8Array, Buffer |
Vec<u8> или срез байтов &[u8] |
Срезы |
WeakMap, WeakRef |
Weak<T> (из Rc или Arc) |
Weak и Cow |
Одна вещь, за которой стоит следить: Map, Set и обычный объект в JavaScript помнят порядок, в котором ты добавлял элементы. HashMap и HashSet в Rust — нет. Если нужен отсортированный порядок, используй BTreeMap; если нужен порядок вставки, крейт indexmap даёт ровно это.
Инструменты: из npm в cargo
Заголовок раздела «Инструменты: из npm в cargo»В Cargo ты почувствуешь себя как дома. Это менеджер пакетов, инструмент сборки, раннер тестов и запуск скриптов в одном, и каждый проект на Rust пользуется им одинаково.
| JavaScript | Rust | Примечания |
|---|---|---|
node + npm (или pnpm, yarn) |
cargo |
Собирает, запускает, тестирует, добавляет пакеты, публикует. См. Cargo и крейты |
package.json |
Cargo.toml |
|
package-lock.json |
Cargo.lock |
Создаётся автоматически |
node_modules/ |
Папки в каждом проекте нет. Загрузки один раз кешируются в ~/.cargo, результаты сборки попадают в target/ |
|
| npmjs.com | crates.io | Документация по API каждого опубликованного крейта лежит на docs.rs |
npm install zod |
cargo add serde |
|
npm init |
cargo new my_app (cargo new --lib для библиотеки) |
|
npm run dev / build / test |
cargo run / cargo build / cargo test |
Добавь --release для оптимизированной сборки; отладочные сборки намного медленнее |
"scripts" в package.json |
Раздела scripts нет. Используй алиасы cargo в .cargo/config.toml или запускалку задач вроде just |
|
npx some-tool |
cargo install some-tool, затем запусти |
cargo binstall скачивает готовые исполняемые файлы вместо компиляции |
nvm и .nvmrc |
rustup и файл rust-toolchain.toml |
См. Инструменты и установка |
| Рабочие пространства npm / pnpm (монорепозиторий) | Рабочее пространство Cargo (workspace) | Один Cargo.toml наверху перечисляет крейты-участники |
tsc |
rustc, который cargo вызывает за тебя |
|
tsc --noEmit |
cargo check |
Проверяет типы без сборки; намного быстрее |
| ESLint | cargo clippy |
Сотни полезных проверок, официально |
| Prettier | cargo fmt |
Один стандартный стиль для всех |
nodemon, tsx watch |
cargo watch или bacon |
Ставятся через cargo install |
| Jest / Vitest / Mocha | Встроенные #[test] и cargo test (cargo nextest — популярный более быстрый раннер) |
См. Тестирование |
| Снапшоты Jest | insta |
См. Тестирование: дополнительно |
jest.mock() |
mockall или написанные вручную фейки, реализующие твой трейт |
|
| fast-check | proptest |
Тестирование на основе свойств |
| c8 / Istanbul (покрытие) | cargo llvm-cov |
|
| JSDoc / TypeDoc | Doc-комментарии /// в Markdown + cargo doc |
Примеры кода в документации запускаются как тесты. См. Документация |
| VS Code + сервер TypeScript | VS Code + rust-analyzer или JetBrains RustRover | rust-analyzer даёт автодополнение, переход к определению и ошибки прямо в коде |
npm publish |
cargo publish |
Вот небольшой Cargo.toml, чтобы сравнить его с 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"] }Версия вроде "1" означает то же, что "^1" в package.json: любой совместимый релиз 1.x. Фичи (features) — это необязательные части крейта, которые ты включаешь сам; см. Фичи Cargo и cfg.
Библиотеки и фреймворки: «Что использовать вместо…?»
Заголовок раздела «Библиотеки и фреймворки: «Что использовать вместо…?»»Это первое, что спрашивает любой разработчик на JavaScript: «Мы используем Express и Prisma. А что есть в Rust?» Вот ближайшие аналоги. Как и в npm, на crates.io для большинства задач много вариантов; здесь собраны популярные и хорошо поддерживаемые.
Веб и сеть
Заголовок раздела «Веб и сеть»| JavaScript | Rust | Примечания |
|---|---|---|
| Express, Fastify, Koa | axum (мы делаем такой в проекте веб-API), actix-web, Rocket | axum и actix-web широко используются в продакшене |
| Middleware Express | Слои tower и крейт tower-http (CORS, сжатие, трассировка, таймауты) |
axum использует tower напрямую |
fetch, axios |
reqwest |
По умолчанию асинхронный, есть необязательный блокирующий клиент. См. Работа с сетью |
ws, Socket.IO |
WebSocket в axum (фича ws), tokio-tungstenite или socketioxide (сервер Socket.IO) |
|
| Apollo Server (GraphQL) | async-graphql |
|
gRPC (@grpc/grpc-js) |
tonic (с prost для protobuf) |
|
| Генераторы Swagger / OpenAPI | utoipa |
|
jsonwebtoken |
jsonwebtoken |
То же имя, та же идея |
bcrypt |
argon2 или bcrypt |
Доступ к данным
Заголовок раздела «Доступ к данным»| JavaScript | Rust | Примечания |
|---|---|---|
| Prisma, TypeORM | SeaORM (асинхронный, сущности, миграции) или Diesel (запросы проверяются на типы при компиляции) | SeaORM ближе всего к Prisma и TypeORM |
Knex, pg, postgres.js |
sqlx: ты пишешь обычный SQL, а макрос query! проверяет его на настоящей базе данных при компиляции |
Асинхронный, очень популярный, не ORM |
better-sqlite3 |
rusqlite или sqlx с SQLite |
|
| Mongoose, драйвер MongoDB | mongodb (официальный драйвер) |
Схем в стиле Mongoose нет; используются структуры serde |
| ioredis | redis |
В уроке Базы данных sqlx, Diesel и SeaORM показаны на работающем коде.
Повседневные строительные блоки
Заголовок раздела «Повседневные строительные блоки»| JavaScript | Rust | Примечания |
|---|---|---|
JSON.parse / JSON.stringify |
serde + serde_json |
Стандарт; также умеет TOML, YAML и другое. См. JSON и Serde |
| Zod, Yup | Большую часть работы делают типы твоей структуры + serde; validator или garde для правил вроде «не меньше 8 символов» |
Если JSON не совпадает со структурой, разбор завершается ошибкой |
| Lodash | В основном итераторы стандартной библиотеки; itertools для дополнительных вещей вроде unique, sorted_by и chunk_by |
|
| date-fns, Day.js, Luxon | chrono или jiff |
См. Дата и время |
| dotenv | dotenvy |
См. Окружение и процессы |
| Commander, yargs | clap |
См. Проект: CLI-утилита на clap |
| chalk | colored или owo-colors |
|
| winston, pino | tracing (+ tracing-subscriber) или log + env_logger |
См. Логирование |
uuid |
uuid |
|
EventEmitter |
Каналы (std::sync::mpsc или tokio::sync::broadcast для многих слушателей) |
См. Каналы |
| BullMQ (очереди задач) | tokio::spawn для задач внутри процесса; apalis для постоянных очередей задач |
|
| Регулярные выражения | regex |
Без lookahead и обратных ссылок, так задумано. См. Регулярные выражения |
Фронтенд, десктоп и Node.js
Заголовок раздела «Фронтенд, десктоп и Node.js»| JavaScript | Rust | Примечания |
|---|---|---|
| Ускорение тяжёлого кода в браузере | Компилируй Rust в WebAssembly с помощью wasm-bindgen и wasm-pack |
См. Rust и WebAssembly и раздел ниже |
| React, Vue, Svelte | Leptos, Yew, Dioxus (все компилируются в WebAssembly) |
Все до версии 1.0; API ещё меняются |
| Electron | Tauri: твой веб-фронтенд + бэкенд на Rust, приложения намного меньше |
Tauri 2 стабилен. См. GUI-приложения |
| Нативные аддоны Node.js (C++ с node-gyp) | napi + napi-derive (napi-rs) или neon |
Публикуешь в npm готовые исполняемые файлы; C++ не нужен |
| Игры на Phaser, Three.js | Bevy, macroquad |
Bevy ещё до 1.0 и ломает API между релизами. См. Разработка игр |
Бок о бок: интерфейс становится структурой
Заголовок раздела «Бок о бок: интерфейс становится структурой»Вот объект на TypeScript с интерфейсом, функцией в роли метода и копией через spread:
interface User { name: string; age: number; admin: boolean;}
const greeting = (u: User) => `Привет, я ${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);В Rust форма объекта становится структурой, а функция — методом в блоке impl:
#[derive(Debug, Clone)]struct User { name: String, age: u32, admin: bool,}
impl User { // `&self` означает «этот метод читает пользователя, но не меняет его». fn greeting(&self) -> String { format!("Привет, я {} ({})", self.name, self.age) }}
fn main() { let ada = User { name: "Ada".to_string(), age: 36, admin: false };
// Как { ...ada, admin: true } в JavaScript. let promoted = User { admin: true, ..ada.clone() };
println!("{}", ada.greeting()); println!("{:?}", promoted);}Вывод:
Привет, я Ada (36)User { name: "Ada", age: 36, admin: true }Отличий несколько. У каждого поля точный тип: age — это u32 (целое число, которое не может быть отрицательным), а не какой-то общий number. #[derive(Debug)] позволяет вывести всю структуру через {:?}, как console.log(obj). А зачем .clone()? Spread ..ada переместил бы строку name из ada, и тогда ada больше нельзя было бы использовать. Клонирование сохраняет ada целым. Это владение в действии, и ты будешь встречать его часто.
Бок о бок: размеченное объединение становится enum
Заголовок раздела «Бок о бок: размеченное объединение становится enum»Разработчики на TypeScript любят размеченные объединения: поле kind говорит, какая у тебя форма, а switch сужает тип:
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 это встроено в язык, и это одна из его лучших возможностей. Каждый вариант перечисления (enum) несёт свои данные, а match разбирает их:
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)); }}Вывод:
3.146.00Строка kind не нужна: сам вариант и есть метка. А match должен покрыть все варианты. Если позже добавить Triangle, каждый match, который о нём забыл, станет ошибкой компиляции. Это похоже на трюк с never для проверки полноты в TypeScript, только включено всегда. Подробнее — в уроке Сопоставление с образцом.
Бок о бок: undefined и null становятся Option
Заголовок раздела «Бок о бок: undefined и null становятся Option»В TypeScript необязательные значения появляются как undefined, null или необязательное свойство, и ты справляешься с ними через ?. и ??:
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() ?? "нет email"}`);}В Rust значение, которого может не быть, имеет тип Option<T>. Это либо Some(value), либо None, и его нельзя использовать как обычное значение, пока ты не обработал случай 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, }}
// Внутри функции, которая возвращает Option, `?` работает как `?.`:// если значение — None, остановись здесь и верни 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] { // Как findUser(id)?.email?.toUpperCase() ?? "нет email" let email = find_user(id) .and_then(|user| user.email) .map(|email| email.to_uppercase()) .unwrap_or("нет email".to_string()); println!("{id}: {email}"); }
println!("{:?}", email_domain(1)); println!("{:?}", email_domain(2));
if let Some(user) = find_user(2) { println!("найден {}", user.name); }}Вывод:
2: нет email3: нет emailSome("example.com")Noneнайден GraceВ Rust нет ошибки «cannot read properties of undefined», потому что нет undefined, из которого можно читать. Компилятор просто не даст вызвать .to_uppercase() у Option<String>. И не нужно помнить разницу между null и undefined: есть только None. Вспомогательные методы описаны в уроке Option, а короткие формы — в уроке if let и while let.
Бок о бок: try/catch становится Result и ?
Заголовок раздела «Бок о бок: try/catch становится Result и ?»В JavaScript функция бросает ошибку, и ты ловишь её где-то выше по стеку вызовов. Сигнатура функции ничего не говорит о том, что она может бросить ошибку:
function parseAge(input) { const age = Number(input); if (!Number.isInteger(age)) throw new Error(`'${input}' — не целое число`); if (age > 150) throw new Error(`${age} — слишком большой возраст`); 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(`Итого: ${totalAge(a, b)}`); } catch (e) { console.log(`Ошибка: ${e.message}`); }}В Rust функция, которая может завершиться неудачей, возвращает Result: либо Ok(value), либо Err(error). Оператор ? означает «если здесь произошла ошибка, сразу верни её вызывающему». Он делает то же, что всплытие исключения, но это видно в коде:
fn parse_age(input: &str) -> Result<u32, String> { let age: u32 = input .parse() .map_err(|_| format!("'{input}' — не целое число"))?;
if age > 150 { return Err(format!("{age} — слишком большой возраст")); } Ok(age)}
fn total_age(a: &str, b: &str) -> Result<u32, String> { // Каждый `?` передаёт ошибку вызывающему, как это сделало бы исключение. 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}"), Err(e) => println!("Ошибка: {e}"), } }}Вывод:
Итого: 77Ошибка: 'abc' — не целое числоОшибка: 200 — слишком большой возрастСигнатура -> Result<u32, String> говорит каждому вызывающему «это может не получиться», и компилятор не даст это проигнорировать. С асинхронным кодом то же самое: там, где в JavaScript был бы отклонённый Promise, асинхронная функция в Rust возвращает Result, и ты пишешь .await?. Для своих типов ошибок смотри урок Свои типы ошибок. В приложениях крейт anyhow даёт что-то близкое к «поймать любую ошибку» (см. anyhow и thiserror).
Бок о бок: методы массивов становятся итераторами
Заголовок раздела «Бок о бок: методы массивов становятся итераторами»У методов массивов, которыми ты пользуешься каждый день, есть прямые аналоги в 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}`);console.log(`Первый крупный заказ: ${firstBig?.customer ?? "нет"}`);console.log(`Есть неоплаченные? ${orders.some((o) => !o.paid)}`);Версия на Rust использует итераторы и замыкания. Синтаксис замыкания — |o| o.paid вместо (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, затем collect() превращает результат в Vec. let paid_names: Vec<&str> = orders .iter() .filter(|o| o.paid) .map(|o| o.customer.as_str()) .collect();
// reduce с начальным значением называется fold. let revenue = orders .iter() .filter(|o| o.paid) .fold(0, |sum, o| sum + o.total);
// find возвращает Option, потому что совпадения может и не быть. let first_big = orders.iter().find(|o| o.total > 150);
println!("{}", paid_names.join(", ")); println!("Выручка: {revenue}"); match first_big { Some(o) => println!("Первый крупный заказ: {}", o.customer), None => println!("Первый крупный заказ: нет"), } // some() называется any(). println!("Есть неоплаченные? {}", orders.iter().any(|o| !o.paid));}Вывод:
Ada, LinusВыручка: 320Первый крупный заказ: LinusЕсть неоплаченные? trueЕсть одно большое отличие от JavaScript. В JS каждый метод массива сразу строит новый массив целиком. Итераторы в Rust ленивые: filter и map сами по себе ничего не делают, а вся работа выполняется за один проход, когда ты вызываешь что-то вроде collect(), sum(), fold() или запускаешь цикл for. Никаких промежуточных массивов не создаётся. Для одной только суммы можно было бы написать и .map(|o| o.total).sum::<u32>(). Сортировка живёт у Vec (sort, sort_by), а не у итераторов; см. Сортировка и поиск.
Бок о бок: Promise становятся future + tokio
Заголовок раздела «Бок о бок: Promise становятся future + tokio»Вот немного асинхронного JavaScript. Следи, когда выводится "загружаем 1":
async function fetchUser(id) { console.log(`загружаем ${id}`); await new Promise((resolve) => setTimeout(resolve, 100)); return `пользователь ${id}`;}
const promise = fetchUser(1); // сразу выводит "загружаем 1"console.log("promise создан");console.log(await promise);
const [a, b] = await Promise.all([fetchUser(2), fetchUser(3)]);console.log(`${a}, ${b}`);В JavaScript вызов асинхронной функции сразу её запускает. В Rust всё иначе: вызов async fn только создаёт future — описание работы. Ничего не выполняется, пока ты не сделаешь .await. А встроенного цикла событий в Rust нет, поэтому ты добавляешь среду выполнения: tokio (на английском) — самая популярная.
use std::time::Duration;
async fn fetch_user(id: u32) -> String { println!("загружаем {id}"); tokio::time::sleep(Duration::from_millis(100)).await; format!("пользователь {id}")}
#[tokio::main]async fn main() { // Создание future НЕ запускает его. Пока ничего не выведено. let future = fetch_user(1); println!("future создан"); println!("{}", future.await);
// Как Promise.all: запустить оба одновременно и дождаться обоих. let (a, b) = tokio::join!(fetch_user(2), fetch_user(3)); println!("{a}, {b}");}Вывод:
future созданзагружаем 1пользователь 1загружаем 2загружаем 3пользователь 2, пользователь 3Видишь порядок? В JavaScript «загружаем 1» появилось бы до «promise создан». В Rust оно идёт после «future создан», потому что future запускается только на .await. #[tokio::main] настраивает среду выполнения (твой цикл событий) и запускает в ней main. tokio::join! ждёт оба future вместе, как Promise.all; tokio::spawn запускает задачу в фоне — это ближе всего к вызову асинхронной функции без await. Подробности — в уроках Async/Await и Async подробно.
Rust в твоём JavaScript-проекте: WebAssembly и аддоны для Node
Заголовок раздела «Rust в твоём JavaScript-проекте: WebAssembly и аддоны для Node»Чтобы начать пользоваться Rust, не нужно ничего переписывать. Вот два распространённых способа подмешать его в JavaScript-проект.
WebAssembly в браузере. Пометь функции на Rust атрибутом #[wasm_bindgen], собери через wasm-pack и импортируй результат, как любой ES-модуль. wasm-pack даже пишет файл .d.ts, так что TypeScript знает типы твоих функций:
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(); // скачать и запустить файл .wasmconsole.log(slugify("Hello Rust World")); // "hello-rust-world"Урок Rust и WebAssembly проведёт тебя через всю настройку и объяснит, какие значения дёшево передаются через границу. Совет: Wasm окупается на тяжёлых вычислениях (обработка изображений, разбор данных, симуляции), а не на маленьких функциях, которые вызываются тысячи раз, потому что каждый вызов между JS и Wasm немного стоит.
Нативные аддоны для Node.js. С napi-rs (на английском; крейты napi и napi-derive) ты помечаешь функцию на Rust атрибутом #[napi], и она становится функцией, которую можно получить через require или import в Node. Многие популярные npm-пакеты так и поставляют готовые исполняемые файлы на Rust, поэтому пользователям никогда не нужен компилятор Rust.
Обо что споткнётся разработчик на JavaScript
Заголовок раздела «Обо что споткнётся разработчик на JavaScript»Все, кто приходит из JavaScript, натыкаются на одни и те же кочки. Знать их заранее полезно.
Передача значения в функцию перемещает его. В JavaScript при передаче массива в функцию передаётся ссылка, и массивом можно пользоваться дальше. В Rust передача Vec или String по значению перемещает (move) владение в функцию, и исходную переменную больше нельзя использовать:
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 moveКомпилятор говорит, что значение names уже перемещено в print_all. Обычное исправление — заимствовать: пусть функция принимает names: &[String], а вызов будет print_all(&names). Сначала прочитай уроки Владение и Заимствование: они объясняют это правило, а встречаться с ним ты будешь каждый день.
У чисел есть типы, и они не смешиваются. В JavaScript один number. В Rust 3 — целое число, а 2.5 — дробное, и без преобразования их не объединить:
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}`Компилятор говорит, что не умеет умножать целое число на дробное. Пиши items as f64 * price или сразу сделай items дробным (3.0). И учти, что целочисленное деление отбрасывает остаток: 7 / 2 равно 3, а не 3.5. См. Типы данных и Приведение типов.
Ещё несколько кочек, на которые ты наткнёшься:
- Future ленивые, и им нужна среда выполнения. Как ты видел выше, ничего не происходит до
.await, а чтобы вообще запустить асинхронный код, нужен tokio (или другая среда выполнения). Если забыть.await, получишь предупреждение компилятора, а не запущенную задачу. - Строки нельзя индексировать.
name[0]не скомпилируется, потому чтоStringв Rust хранится в UTF-8, и один символ может занимать несколько байтов. Используйname.chars().next()или.chars().nth(i). И пораньше разберись в разнице междуStringи&str: функции обычно принимают&str, структуры обычно хранятString. См. Строки. - Нет структурной типизации и нет
any. В TypeScript любой объект с нужными полями подходит под интерфейс. В Rust функция, которая принимаетUser, принимает толькоUser. Для общего поведения используй трейты. Для JSON неизвестной формы —serde_json::Value. - Общие изменяемые графы объектов даются тяжело. В JavaScript нормально передавать один объект повсюду и разрешать всем его менять или делать объекты, которые ссылаются обратно на родителя. Анализатор заимствований (borrow checker) разрешает одновременно много читателей или одного писателя, но никогда и то и другое. Такие схемы требуют умных указателей (
Rc<RefCell<T>>,Arc<Mutex<T>>) или, что лучше, другой архитектуры. Приготовься к нескольким неделям раздражения. Потом станет намного легче. - У
HashMapнет порядка.Mapи объекты в JavaScript сохраняют порядок вставки, и код часто незаметно на это полагается.HashMapв Rust — нет. Сортируй перед выводом или используйBTreeMapлибо крейтindexmap. - Нет исключений. Ошибки — это значения, и ты будешь часто писать
?. Вызывать.unwrap()во время обучения нормально, но это версия Rust для!из TypeScript: «поверь мне, значение там есть». Если ты ошибся, программа паникует. - Время компиляции. Мгновенной перезагрузки, как у
node app.js, нет. Первая сборка с множеством крейтов занимает время; следующие сборки инкрементальные и быстрее. Пока редактируешь, используйcargo check: он пропускает генерацию кода.
Путь обучения для разработчиков на JavaScript
Заголовок раздела «Путь обучения для разработчиков на JavaScript»Этот сайт не обязательно читать по порядку. Для разработчика на JavaScript или TypeScript этот маршрут пропускает то, что ты уже знаешь, и тратит время там, где Rust отличается:
- Первые шаги и Инструменты и установка: установи Rust, настрой VS Code с rust-analyzer, запусти
cargo new. - Переменные и типы данных: прочитай их как следует. Числовые типы для тебя новые.
- Владение, затем заимствование, затем срезы: самые важные для тебя страницы. Не торопись.
- Строки:
Stringи&str. - Структуры и перечисления, затем match и сопоставление с образцом: твои интерфейсы и размеченные объединения.
- Option и Result, затем свои типы ошибок.
- Замыкания и итераторы: твои методы массивов.
- Векторы, HashMap и HashSet.
- Трейты, обобщения и популярные трейты.
- Модули, Cargo и крейты и тестирование.
- Времена жизни и умные указатели.
- Async/Await и Async подробно.
- JSON и Serde, а потом собери проект веб-API или попробуй WebAssembly.
Практика
Заголовок раздела «Практика»В обоих упражнениях дан фрагмент на JavaScript или TypeScript. Перенеси его на Rust.
1. Посчитай слова с помощью Map
Заголовок раздела «1. Посчитай слова с помощью Map»Этот код на JavaScript считает, сколько раз встречается каждое слово, а затем выводит слова от самых частых к самым редким (при равенстве — по алфавиту). Перенеси его на Rust. Подсказка: используй HashMap<&str, u32> и *counts.entry(word).or_insert(0) += 1. Чтобы отсортировать, собери словарь в Vec пар (слово, количество) и используй sort_by.
const text = "the cat and the hat and the bat";
const counts = new Map();for (const word of text.split(" ")) { counts.set(word, (counts.get(word) ?? 0) + 1);}
const sorted = [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));for (const [word, n] of sorted) console.log(`${word}: ${n}`);Показать решение
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; }
// Как [...counts]: превращаем словарь в список пар (слово, количество). let mut sorted: Vec<(&str, u32)> = counts.into_iter().collect(); // Сначала самые частые; при равенстве — по алфавиту. sorted.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (word, n) in sorted { println!("{word}: {n}"); }}Вывод:
the: 3and: 2bat: 1cat: 1hat: 1entry(word).or_insert(0) за один шаг делает и часть counts.get(word) ?? 0, и set: он даёт изменяемую ссылку на счётчик и создаёт его со значением 0, если слова ещё нет. Сортировка здесь обязательна, а не просто приятна: без неё HashMap выводит элементы в разном порядке от запуска к запуску. Слова в примере оставлены английскими, чтобы сортировка по алфавиту давала тот же результат, что и в оригинале.
2. Перенеси необязательные настройки со значениями по умолчанию
Заголовок раздела «2. Перенеси необязательные настройки со значениями по умолчанию»Эта функция на TypeScript принимает объект настроек, где каждое поле необязательно, и заполняет значения по умолчанию через ??. Перенеси её на Rust. Подсказка: сделай структуру с полями Option и добавь #[derive(Default)]. Тогда ServerOptions { port: Some(3000), ..Default::default() } работает как передача { port: 3000 }. Для bool значение по умолчанию — 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 ? "подробно" : "тихо"; return `${host}:${port} (${mode})`;}
console.log(describe({}));console.log(describe({ port: 3000 }));console.log(describe({ host: "example.com", verbose: true }));Показать решение
#[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 { "подробно" } else { "тихо" }; 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() }) );}Вывод:
localhost:8080 (тихо)localhost:3000 (тихо)example.com:8080 (подробно)unwrap_or — это твой ??. as_deref() превращает Option<String> в Option<&str>, чтобы можно было подставить обычный текст "localhost" без всякого копирования. А ..Default::default() — это способ Rust сказать «каждое поле, которое я не упомянул, получает значение по умолчанию», то есть ровно то, что в JavaScript делает пропущенное свойство. А u16 для порта означает, что порт вроде 70000 даже нельзя случайно сохранить.
- Многое из JavaScript и TypeScript переносится: замыкания, async/await, деструктуризация и методы в стиле массивов вроде
map,filterиfind— у всего этого есть близкие аналоги в Rust. - Главные сдвиги: владение вместо сборщика мусора,
OptionвместоundefinedиnullиResult+?вместоthrowиtry/catch. - Интерфейсы TypeScript становятся структурами (для данных) или трейтами (для поведения), а размеченные объединения — перечислениями с
match. - Future в Rust ленивые, и им нужна среда выполнения вроде tokio; роль
Promise.allиграетtokio::join!. - Cargo заменяет npm,
package.jsonи твои скрипты; clippy и rustfmt заменяют ESLint и Prettier; тестирование встроено. - Вместо Express смотри на axum или actix-web, вместо
fetch— на reqwest, вместо Prisma — на SeaORM или Diesel, вместо Electron — на Tauri. А ещё Rust можно принести в JS-проект через WebAssembly или napi-rs.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Что Rust использует вместо undefined и null?
2. В JavaScript вызов асинхронной функции сразу её запускает. Что происходит, если вызвать async fn в Rust без .await?
3. В твоём коде на TypeScript есть размеченное объединение с полем kind. Какая возможность Rust ближе всего?
4. Бэкенд твоей команды написан на Express. Какой крейт Rust ближе всего?
Результат: 0 / 4
Что почитать
Заголовок раздела «Что почитать»- Язык программирования Rust: официальная книга (перевод на русский). Для разработчиков на JavaScript важнее всего главы 4 (владение), 6 (перечисления и сопоставление с образцом) и 9 (ошибки).
- Tokio tutorial (на английском): официальное руководство по асинхронному Rust с tokio — от первой задачи до каналов и ввода-вывода.
- The wasm-bindgen Guide (на английском): как Rust и JavaScript общаются друг с другом в WebAssembly, с множеством примеров.
- Compiling from Rust to WebAssembly (MDN) (на английском): пошаговое руководство из MDN Web Docs, включая публикацию в npm.
- napi-rs (на английском): документация по написанию аддонов для Node.js на Rust.