diff --git a/typescript-book/book.toml b/typescript-book/book.toml new file mode 100644 index 0000000..5a2f80c --- /dev/null +++ b/typescript-book/book.toml @@ -0,0 +1,11 @@ +[book] +title = "Rust for TypeScript Programmers" +description = "A comprehensive guide to learning Rust for developers with TypeScript experience" +authors = ["Microsoft"] +language = "en" + +[preprocessor.mermaid] +command = "mdbook-mermaid" + +[output.html] +git-repository-url = "https://github.com/microsoft/RustTraining" diff --git a/typescript-book/src/SUMMARY.md b/typescript-book/src/SUMMARY.md new file mode 100644 index 0000000..3f9483a --- /dev/null +++ b/typescript-book/src/SUMMARY.md @@ -0,0 +1,49 @@ +# Summary + +[Introduction](ch00-introduction.md) + +--- + +# Part I — Foundations + +- [1. Introduction and Motivation](ch01-introduction-and-motivation.md) +- [2. Getting Started](ch02-getting-started.md) +- [3. Built-in Types](ch03-built-in-types.md) +- [4. Control Flow](ch04-control-flow.md) +- [5. Data Structures and Collections](ch05-data-structures-and-collections.md) +- [6. Enums and Pattern Matching](ch06-enums-and-pattern-matching.md) + +--- + +# Part II — Core Concepts + +- [7. Ownership and Borrowing](ch07-ownership-and-borrowing.md) + - [Lifetimes for TypeScript Developers](ch07-1-lifetimes.md) +- [8. Crates and Modules](ch08-crates-and-modules.md) + - [Testing Patterns](ch08-1-testing-patterns.md) +- [9. Error Handling](ch09-error-handling.md) + - [Error Handling Best Practices](ch09-1-error-handling-best-practices.md) +- [10. Traits and Generics](ch10-traits-and-generics.md) + - [Generics Deep Dive](ch10-1-generics-deep-dive.md) +- [11. From and Into Traits](ch11-from-and-into-traits.md) +- [12. Closures and Iterators](ch12-closures-and-iterators.md) + +--- + +# Part III — Advanced Topics & Migration + +- [13. Concurrency](ch13-concurrency.md) + - [Async/Await — From Promises to Futures](ch13-1-async-await.md) +- [14. Unsafe Rust and FFI](ch14-unsafe-rust-and-ffi.md) + - [WebAssembly and wasm-bindgen](ch14-1-webassembly.md) +- [15. Migration Patterns](ch15-migration-patterns.md) +- [16. Best Practices](ch16-best-practices.md) + - [Avoiding Excessive clone()](ch16-1-avoiding-excessive-clone.md) + - [Logging and Tracing Ecosystem](ch16-2-logging-and-tracing-ecosystem.md) +- [17. TypeScript → Rust Semantic Deep Dives](ch17-ts-rust-semantic-deep-dives.md) + +--- + +# Part IV — Capstone + +- [18. Capstone Project: REST API Server](ch18-capstone-project.md) diff --git a/typescript-book/src/ch00-introduction.md b/typescript-book/src/ch00-introduction.md new file mode 100644 index 0000000..de7b734 --- /dev/null +++ b/typescript-book/src/ch00-introduction.md @@ -0,0 +1,77 @@ +# Rust for TypeScript Programmers: Complete Training Guide + +A comprehensive guide to learning Rust for developers with TypeScript experience. This guide +covers everything from basic syntax to advanced patterns, focusing on the conceptual shifts +required when moving from a dynamically-typed runtime with a garbage collector and an optional +type system to a statically-typed systems language with compile-time memory safety. + +## How to Use This Book + +**Self-study format**: Work through Part I (ch 1–6) first — these map closely to TypeScript +concepts you already know and will feel familiar. Part II (ch 7–12) introduces the ideas that +make Rust *different*: ownership, borrowing, lifetimes, and traits. These are where TypeScript +developers typically need the most time, so take it slow. Part III (ch 13–17) covers advanced +topics, migration strategies, and the async runtime model. Part IV wraps up with a capstone +project that ties everything together. + +**Workshop format (3 days)**: + +| Day | Chapters | Theme | +|-----|----------|-------| +| 1 | 1 – 6 | Foundations: types, data, control flow | +| 2 | 7 – 12 | Core: ownership, traits, generics, iterators | +| 3 | 13 – 18 | Advanced: async, Wasm, migration, capstone | + +**Quick reference**: Each chapter starts with a "TypeScript ↔ Rust" comparison table so you can +scan for equivalents fast. Side-by-side code blocks are used throughout to build on your +existing TypeScript knowledge. + +## What You Already Know (and How It Helps) + +Coming from TypeScript, you already have a strong foundation: + +- **Static types** — You understand type annotations, generics, union types, and structural + typing. Rust's type system will feel both familiar and stricter. +- **Algebraic data types** — TypeScript's discriminated unions (`type Shape = Circle | Square`) + are conceptually close to Rust enums. +- **Generics** — You're used to `Array`, `Promise`, and generic functions. Rust generics + work similarly but are monomorphized at compile time. +- **Async/await** — TypeScript's `async`/`await` over Promises maps to Rust's `async`/`await` + over Futures, though the execution model is very different. +- **Module systems** — TypeScript's ES module imports/exports have clear parallels to Rust's + `mod`, `use`, and `pub`. +- **Toolchain** — If you're comfortable with `npm`, `tsc`, `eslint`, and `prettier`, you'll + find `cargo`, `rustc`, `clippy`, and `rustfmt` refreshingly similar in purpose. + +## What Will Be New + +These are the concepts without direct TypeScript equivalents: + +- **Ownership and borrowing** — No garbage collector. The compiler enforces memory safety rules + at compile time. +- **Lifetimes** — Explicit annotations that tell the compiler how long references live. +- **Move semantics** — Assigning a value can *move* it, making the original binding invalid. +- **No null, no undefined** — `Option` and `Result` replace nullable types. +- **No classes** — Structs + traits replace class-based OOP. +- **No runtime reflection** — No `typeof` at runtime, no `Object.keys()` on arbitrary types. +- **Manual string handling** — Multiple string types (`String`, `&str`, `OsString`, …) instead + of one universal `string`. +- **No exceptions** — Errors are values, not thrown. `?` replaces `try/catch` in most cases. + +## Conventions + +Throughout this book: + +- 🟦 **TypeScript** code blocks show the familiar pattern. +- 🦀 **Rust** code blocks show the idiomatic Rust equivalent. +- 💡 **Key insight** callouts explain the conceptual shift. +- ⚠️ **Common pitfall** callouts warn about traps TypeScript developers commonly fall into. +- 🏋️ **Exercise** callouts provide hands-on practice. + +## Prerequisites + +- Comfortable writing TypeScript (basic generics, async/await, union types). +- A working Rust installation: [rustup.rs](https://rustup.rs). +- An editor with `rust-analyzer` (VS Code recommended — you likely already have it). + +Let's get started! diff --git a/typescript-book/src/ch01-introduction-and-motivation.md b/typescript-book/src/ch01-introduction-and-motivation.md new file mode 100644 index 0000000..9a1a704 --- /dev/null +++ b/typescript-book/src/ch01-introduction-and-motivation.md @@ -0,0 +1,98 @@ +# Introduction and Motivation + +## Why Rust for TypeScript Developers? + +As a TypeScript developer, you chose TypeScript over JavaScript for a reason: you value type +safety, better tooling, and catching bugs early. Rust takes that philosophy to its logical +extreme — every category of bug that TypeScript's type system can't catch (null pointer +dereferences at runtime, data races, use-after-free) is caught at compile time in Rust. + +### Where TypeScript Falls Short + +TypeScript improves on JavaScript, but it still inherits fundamental limitations: + +- **Runtime overhead** — V8 (or Deno/Bun) adds GC pauses, JIT warm-up, and memory overhead. +- **Type erasure** — Types vanish at runtime. A `User` type offers zero runtime guarantees. +- **`any` escape hatch** — One `any` can silently break an entire type chain. +- **Concurrency model** — Single-threaded event loop. True parallelism requires worker threads + with message passing and serialization overhead. +- **No memory control** — You cannot control allocations, layout, or lifetimes. + +### What Rust Offers + +| Concern | TypeScript | Rust | +|---------|-----------|------| +| Null safety | Optional (`strictNullChecks`) | Enforced (`Option`) | +| Error handling | Thrown exceptions (unchecked) | `Result` (checked) | +| Memory management | Garbage collector | Ownership system (zero-cost) | +| Concurrency | Single-threaded + workers | Fearless concurrency (threads, async) | +| Performance | JIT-compiled, GC pauses | Compiled to native, no runtime | +| Type guarantees | Erased at runtime | Present at compile time, monomorphized | +| Package manager | npm / yarn / pnpm | cargo (built-in) | + +### When to Reach for Rust + +Rust is not a replacement for TypeScript in every scenario. Use Rust when you need: + +- **Performance-critical services** — HTTP servers, data pipelines, real-time systems. +- **WebAssembly modules** — Ship compiled Rust to the browser alongside your TypeScript app. +- **CLI tools** — Fast startup, single binary, no runtime dependency. +- **Embedded / systems** — Where a GC and runtime are not available. +- **Correctness guarantees** — When "it compiled, therefore it works" matters. + +Keep using TypeScript for rapid UI prototyping, full-stack web apps where developer velocity +is paramount, and anywhere the Node.js ecosystem gives you a critical advantage. + +## Toolchain Comparison + +If you're used to the TypeScript toolchain, here's how Rust's tools map: + +| TypeScript | Rust | Purpose | +|-----------|------|---------| +| `npm` / `yarn` / `pnpm` | `cargo` | Package management and task runner | +| `package.json` | `Cargo.toml` | Project manifest | +| `node_modules/` | `~/.cargo/registry/` | Dependency cache | +| `tsc` | `rustc` | Compiler | +| `tsconfig.json` | `Cargo.toml` + `rustfmt.toml` | Compiler and formatter config | +| `eslint` | `clippy` | Linter | +| `prettier` | `rustfmt` | Formatter | +| `jest` / `vitest` | `cargo test` | Test runner (built-in) | +| `tsx` / `ts-node` | `cargo run` | Run a project | +| `npmjs.com` | `crates.io` | Package registry | +| `tsdoc` / `typedoc` | `cargo doc` / `rustdoc` | Documentation generator | + +## Hello, Rust! + +Let's compare a minimal program: + +🟦 **TypeScript** +```typescript +function greet(name: string): string { + return `Hello, ${name}!`; +} + +console.log(greet("TypeScript")); +``` + +🦀 **Rust** +```rust +fn greet(name: &str) -> String { + format!("Hello, {name}!") +} + +fn main() { + println!("{}", greet("Rust")); +} +``` + +💡 **Key differences to notice**: +- `fn` instead of `function`. +- Return type comes after `->`, not before with `:`. +- No `return` keyword needed — the last expression is the return value (no semicolon). +- `&str` is a *borrowed* string slice; `String` is an *owned* heap string. We'll cover this + distinction in depth in Chapter 7. +- `main()` is the entry point — there's no top-level execution like Node.js. +- `println!` is a *macro* (note the `!`), not a function. + +🏋️ **Exercise**: Create a new project with `cargo new hello-ts` and modify `src/main.rs` to +print a greeting. Run it with `cargo run`. diff --git a/typescript-book/src/ch02-getting-started.md b/typescript-book/src/ch02-getting-started.md new file mode 100644 index 0000000..5195096 --- /dev/null +++ b/typescript-book/src/ch02-getting-started.md @@ -0,0 +1,114 @@ +# Getting Started + +## Installing Rust + +```bash +# Install rustup (manages Rust toolchains) +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + +# Verify installation +rustc --version +cargo --version +``` + +This is analogous to installing Node.js via `nvm` — `rustup` manages compiler versions the way +`nvm` manages Node versions. + +## Creating a Project + +🟦 **TypeScript** +```bash +mkdir my-project && cd my-project +npm init -y +# edit package.json, install typescript, create tsconfig.json… +``` + +🦀 **Rust** +```bash +cargo new my-project +cd my-project +# That's it. Cargo.toml and src/main.rs are ready. +``` + +`cargo new` creates: + +``` +my-project/ +├── Cargo.toml # ≈ package.json +└── src/ + └── main.rs # entry point +``` + +### Cargo.toml vs package.json + +```toml +[package] +name = "my-project" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1", features = ["derive"] } +tokio = { version = "1", features = ["full"] } +``` + +Compared to `package.json`, there are no `devDependencies` vs `dependencies` split for the +binary itself — `[dev-dependencies]` exists but is only for test/build-time crates. + +## Cargo Commands You'll Use Daily + +| Command | Purpose | TypeScript equivalent | +|---------|---------|----------------------| +| `cargo build` | Compile (debug) | `tsc` | +| `cargo build --release` | Compile (optimized) | `tsc` + bundler | +| `cargo run` | Build + run | `tsx src/index.ts` | +| `cargo test` | Run all tests | `vitest run` | +| `cargo clippy` | Lint | `eslint .` | +| `cargo fmt` | Format | `prettier --write .` | +| `cargo doc --open` | Generate and view docs | `typedoc` | +| `cargo add serde` | Add a dependency | `npm install serde` | + +## Editor Setup + +If you use VS Code (most TypeScript developers do): + +1. Install the **rust-analyzer** extension (replaces the older Rust extension). +2. Install **CodeLLDB** for debugging. +3. Optional: **Even Better TOML** for `Cargo.toml` syntax highlighting. + +`rust-analyzer` provides the same kind of experience you get from TypeScript's language +server — inline type hints, go-to-definition, auto-imports, and real-time error checking. + +## Your First Cargo Project + +```rust +// src/main.rs +fn main() { + let language = "Rust"; // type inferred as &str + let year: u32 = 2015; // explicit type annotation + println!("{language} was released in {year}"); +} +``` + +Run it: +```bash +$ cargo run + Compiling my-project v0.1.0 + Finished dev [unoptimized + debuginfo] target(s) + Running `target/debug/my-project` +Rust was released in 2015 +``` + +💡 **Key insight**: `let` in Rust is *immutable by default*. To mutate a variable, use +`let mut`. This is the opposite of TypeScript, where `let` is mutable and `const` is +immutable. + +```rust +let x = 5; +// x = 6; // ❌ error: cannot assign twice to immutable variable +let mut y = 5; +y = 6; // ✅ ok +``` + +🏋️ **Exercise**: Create a project with `cargo new playground`, add the `chrono` crate with +`cargo add chrono`, and print today's date. diff --git a/typescript-book/src/ch03-built-in-types.md b/typescript-book/src/ch03-built-in-types.md new file mode 100644 index 0000000..2184dbf --- /dev/null +++ b/typescript-book/src/ch03-built-in-types.md @@ -0,0 +1,196 @@ +# Built-in Types + +## Type System Philosophy + +TypeScript's type system is *structural* and *optional* — types are checked by shape, and you +can escape with `any`. Rust's type system is *nominal* and *mandatory* — every value has exactly +one concrete type, determined at compile time, and there is no escape hatch. + +## Scalar Types + +### Numbers + +🟦 **TypeScript** — one `number` type (64-bit float) plus `bigint`: +```typescript +const count: number = 42; +const price: number = 9.99; +const big: bigint = 9007199254740993n; +``` + +🦀 **Rust** — explicit integer and float sizes: +```rust +let count: i32 = 42; // signed 32-bit integer +let price: f64 = 9.99; // 64-bit float +let small: u8 = 255; // unsigned 8-bit +let big: i128 = 9_007_199_254_740_993; +let arch: usize = 42; // pointer-sized unsigned (like size_t) +``` + +| TypeScript | Rust equivalents | +|-----------|-----------------| +| `number` | `i8`, `i16`, `i32`, `i64`, `i128`, `u8`, `u16`, `u32`, `u64`, `u128`, `f32`, `f64` | +| `bigint` | `i128`, `u128`, or the `num-bigint` crate | + +⚠️ **Common pitfall**: Integer overflow panics in debug mode and wraps in release mode. +TypeScript silently loses precision. Use `checked_add`, `saturating_add`, or `wrapping_add` for +explicit control. + +### Booleans + +Identical concept, slightly different syntax: + +```typescript +const done: boolean = true; +``` + +```rust +let done: bool = true; +``` + +### Characters + +🟦 TypeScript has no character type — single characters are just `string`: +```typescript +const ch: string = "A"; +``` + +🦀 Rust has `char`, a Unicode scalar value (4 bytes): +```rust +let ch: char = 'A'; // single quotes +let emoji: char = '🦀'; +``` + +## Strings + +This is where things diverge the most. + +🟦 **TypeScript** — one `string` type, immutable, UTF-16 internally: +```typescript +const greeting: string = "hello"; +const name: string = "world"; +const message: string = `${greeting}, ${name}!`; +``` + +🦀 **Rust** — two primary string types: + +| Type | Owned? | Mutable? | Where? | Analogy | +|------|--------|----------|--------|---------| +| `String` | Yes | Yes (if `mut`) | Heap | Like a `StringBuilder` | +| `&str` | No (borrowed) | No | Stack/heap/static | Like a read-only view | + +```rust +let greeting: &str = "hello"; // string literal → &str +let name: String = String::from("world"); // heap-allocated String +let message: String = format!("{greeting}, {name}!"); +``` + +💡 **Key insight**: Think of `&str` as a *window* into string data owned by someone else, and +`String` as a string that *you* own and can grow. Most functions take `&str` as input (flexible) +and return `String` as output (owned). + +```rust +// Accepts any string-like input +fn shout(s: &str) -> String { + s.to_uppercase() +} + +let owned = String::from("hello"); +shout(&owned); // &String auto-coerces to &str +shout("hello"); // &str directly +``` + +## The Unit Type — Rust's `void` + +🟦 TypeScript: +```typescript +function log(msg: string): void { + console.log(msg); +} +``` + +🦀 Rust: +```rust +fn log(msg: &str) { + // return type is `()` (unit) — implied when omitted + println!("{msg}"); +} +``` + +The unit type `()` is a real type with exactly one value: `()`. It's like TypeScript's `void` +but can be stored in variables, used as generic parameters, etc. + +## Type Inference + +Both languages have inference, but Rust's is more powerful because types are never erased: + +```rust +let x = 42; // inferred as i32 +let y = 3.14; // inferred as f64 +let names = vec!["a", "b"]; // inferred as Vec<&str> + +// Sometimes you need turbofish syntax to help the compiler: +let parsed = "42".parse::().unwrap(); +// or +let parsed: i32 = "42".parse().unwrap(); +``` + +## Type Aliases + +🟦 TypeScript: +```typescript +type UserId = number; +type Callback = (data: string) => void; +``` + +🦀 Rust: +```rust +type UserId = u64; +type Callback = fn(data: &str); +// or for closures: +type Callback = Box; +``` + +## Arrays and Tuples + +### Fixed-size arrays + +```rust +let rgb: [u8; 3] = [255, 128, 0]; // fixed size, known at compile time +let zeros = [0u8; 100]; // 100 zeros +``` + +### Tuples + +🟦 TypeScript: +```typescript +const pair: [string, number] = ["age", 30]; +const [key, value] = pair; +``` + +🦀 Rust: +```rust +let pair: (&str, i32) = ("age", 30); +let (key, value) = pair; // destructuring +``` + +## Never Type + +🟦 TypeScript: +```typescript +function fail(msg: string): never { + throw new Error(msg); +} +``` + +🦀 Rust: +```rust +fn fail(msg: &str) -> ! { + panic!("{msg}"); +} +``` + +The `!` (never) type means the function never returns. Used for `panic!`, infinite loops, and +`process::exit`. + +🏋️ **Exercise**: Write a function `describe(value: f64) -> String` that returns `"positive"`, +`"negative"`, or `"zero"`. Use type inference where possible. diff --git a/typescript-book/src/ch04-control-flow.md b/typescript-book/src/ch04-control-flow.md new file mode 100644 index 0000000..c5cfb14 --- /dev/null +++ b/typescript-book/src/ch04-control-flow.md @@ -0,0 +1,193 @@ +# Control Flow + +## Expressions vs Statements + +The single biggest conceptual shift: in Rust, almost everything is an **expression** that +returns a value. In TypeScript, `if` is a statement; in Rust, it's an expression. + +🟦 **TypeScript** +```typescript +// if is a statement — you need a ternary or variable +const label = x > 0 ? "positive" : "non-positive"; +``` + +🦀 **Rust** +```rust +// if is an expression — no ternary operator needed +let label = if x > 0 { "positive" } else { "non-positive" }; +``` + +💡 **Key insight**: The last expression in a block (without a semicolon) is the block's return +value. Adding a semicolon turns it into a statement that returns `()`. + +```rust +let val = { + let a = 1; + let b = 2; + a + b // ← no semicolon → this is the block's value (3) +}; +``` + +## `if` / `else if` / `else` + +🟦 **TypeScript** +```typescript +if (temp > 30) { + console.log("hot"); +} else if (temp > 20) { + console.log("warm"); +} else { + console.log("cold"); +} +``` + +🦀 **Rust** +```rust +if temp > 30 { + println!("hot"); +} else if temp > 20 { + println!("warm"); +} else { + println!("cold"); +} +``` + +No parentheses around the condition (they're allowed but `clippy` will warn). Braces are +always required — no single-line `if` without braces. + +## Loops + +### `loop` — infinite loop (no TypeScript equivalent) + +```rust +let mut counter = 0; +let result = loop { + counter += 1; + if counter == 10 { + break counter * 2; // break with a value! + } +}; +// result == 20 +``` + +### `while` + +Nearly identical to TypeScript: + +```rust +let mut n = 0; +while n < 5 { + println!("{n}"); + n += 1; +} +``` + +### `for` — Range-based iteration + +🟦 **TypeScript** +```typescript +for (let i = 0; i < 5; i++) { … } +for (const item of items) { … } +``` + +🦀 **Rust** +```rust +for i in 0..5 { … } // 0, 1, 2, 3, 4 +for i in 0..=5 { … } // 0, 1, 2, 3, 4, 5 (inclusive) +for item in &items { … } // borrow each item +for item in items { … } // consume (move) the collection +for (i, item) in items.iter().enumerate() { … } // index + value +``` + +⚠️ **Common pitfall**: `for item in items` *moves* the vector. After the loop, `items` is no +longer usable. Use `for item in &items` to borrow instead. + +## `match` — TypeScript's `switch` on Steroids + +`match` is Rust's most powerful control flow construct. It's like TypeScript's `switch`, but: +- It must be *exhaustive* — every possible value must be covered. +- It can destructure values. +- It returns a value (it's an expression). + +🟦 **TypeScript** +```typescript +switch (status) { + case "ok": + return 200; + case "not_found": + return 404; + default: + return 500; +} +``` + +🦀 **Rust** +```rust +match status { + "ok" => 200, + "not_found" => 404, + _ => 500, // _ is the wildcard / default +} +``` + +### Pattern Matching with Destructuring + +```rust +let point = (3, -5); +match point { + (0, 0) => println!("origin"), + (x, 0) => println!("on x-axis at {x}"), + (0, y) => println!("on y-axis at {y}"), + (x, y) if x > 0 && y > 0 => println!("quadrant I"), + (x, y) => println!("at ({x}, {y})"), +} +``` + +### `if let` — Match a Single Pattern + +🟦 **TypeScript** (with discriminated unions) +```typescript +if (shape.kind === "circle") { + console.log(shape.radius); +} +``` + +🦀 **Rust** +```rust +if let Shape::Circle { radius } = shape { + println!("{radius}"); +} +``` + +### `let else` — The Inverse + +```rust +let Some(count) = maybe_count else { + return Err("no count provided"); +}; +// count is now available here, unwrapped +``` + +This is similar to TypeScript's early-return pattern with narrowing: +```typescript +if (maybeCount === undefined) { + throw new Error("no count provided"); +} +// maybeCount is now narrowed to number +``` + +## `while let` + +Useful for consuming an iterator or repeatedly matching: + +```rust +let mut stack = vec![1, 2, 3]; +while let Some(top) = stack.pop() { + println!("{top}"); +} +// prints 3, 2, 1 +``` + +🏋️ **Exercise**: Write a `match` expression that takes a letter grade (`'A'`, `'B'`, `'C'`, +`'D'`, `'F'`) and returns the corresponding GPA value as an `f64`. Handle invalid grades with +a default arm. diff --git a/typescript-book/src/ch05-data-structures-and-collections.md b/typescript-book/src/ch05-data-structures-and-collections.md new file mode 100644 index 0000000..cfde595 --- /dev/null +++ b/typescript-book/src/ch05-data-structures-and-collections.md @@ -0,0 +1,249 @@ +# Data Structures and Collections + +## Structs — Rust's Interfaces-Made-Real + +In TypeScript, you define shape with `interface` or `type`, but the runtime object is just a +plain JavaScript object. In Rust, `struct` defines both the shape *and* the memory layout. + +🟦 **TypeScript** +```typescript +interface User { + name: string; + age: number; + email: string; +} + +const alice: User = { name: "Alice", age: 30, email: "alice@example.com" }; +``` + +🦀 **Rust** +```rust +struct User { + name: String, + age: u32, + email: String, +} + +let alice = User { + name: String::from("Alice"), + age: 30, + email: String::from("alice@example.com"), +}; +``` + +### Field Shorthand + +Both languages support shorthand when variable names match field names: + +```typescript +const name = "Alice"; +const user = { name, age: 30 }; // shorthand +``` + +```rust +let name = String::from("Alice"); +let user = User { name, age: 30, email: String::from("a@b.com") }; +``` + +### Struct Update Syntax (Spread) + +🟦 TypeScript: +```typescript +const updated = { ...alice, age: 31 }; +``` + +🦀 Rust: +```rust +let updated = User { age: 31, ..alice }; +``` + +⚠️ **Pitfall**: This *moves* fields from `alice` that aren't `Copy`. After this, `alice.name` +and `alice.email` are moved and can no longer be used (but `alice.age` is fine because integers +are `Copy`). + +## Impl Blocks — Methods on Structs + +TypeScript puts methods inside the class. Rust separates data (`struct`) from behavior (`impl`): + +🟦 **TypeScript** +```typescript +class Rectangle { + constructor(public width: number, public height: number) {} + + area(): number { + return this.width * this.height; + } + + static square(size: number): Rectangle { + return new Rectangle(size, size); + } +} +``` + +🦀 **Rust** +```rust +struct Rectangle { + width: f64, + height: f64, +} + +impl Rectangle { + // Method (takes &self) + fn area(&self) -> f64 { + self.width * self.height + } + + // Associated function (no self — like a static method) + fn square(size: f64) -> Self { + Self { width: size, height: size } + } +} + +let r = Rectangle::square(5.0); +println!("Area: {}", r.area()); +``` + +💡 **Key insight**: `&self` is shorthand for `self: &Self`. Methods borrow `self` by default. +Use `&mut self` if the method needs to mutate, or `self` if it consumes the struct. + +## Tuple Structs and Newtypes + +```rust +struct Meters(f64); +struct Seconds(f64); + +let distance = Meters(100.0); +let time = Seconds(9.58); +// distance + time → compile error! Different types. +``` + +This is like TypeScript's branded types, but enforced at the compiler level rather than +as a convention. + +## Collections + +### Vec — Dynamic Array + +🟦 TypeScript: `Array` / `T[]` +🦀 Rust: `Vec` + +```rust +let mut nums: Vec = Vec::new(); +nums.push(1); +nums.push(2); +nums.push(3); + +// Or use the vec! macro: +let nums = vec![1, 2, 3]; + +// Access +let first = nums[0]; // panics if out of bounds +let first = nums.get(0); // returns Option<&i32> +``` + +### HashMap — Object / Map + +🟦 TypeScript: `Record` / `Map` +🦀 Rust: `HashMap` + +```rust +use std::collections::HashMap; + +let mut scores: HashMap = HashMap::new(); +scores.insert("Alice".to_string(), 100); +scores.insert("Bob".to_string(), 85); + +// Access +if let Some(score) = scores.get("Alice") { + println!("Alice: {score}"); +} + +// Entry API (like Map.has + set) +scores.entry("Charlie".to_string()).or_insert(0); +``` + +### HashSet + +🟦 TypeScript: `Set` +🦀 Rust: `HashSet` + +```rust +use std::collections::HashSet; + +let mut tags: HashSet = HashSet::new(); +tags.insert("rust".to_string()); +tags.insert("wasm".to_string()); +tags.contains("rust"); // true +``` + +### BTreeMap / BTreeSet + +Sorted versions of `HashMap` and `HashSet`. Use when you need ordered keys. + +### VecDeque + +Double-ended queue — efficient push/pop from both ends. TypeScript equivalent: +`Array` used as a deque (but `Array.shift()` is O(n) in JS, while `VecDeque::pop_front()` +is O(1)). + +## Option and Result — No More null / undefined + +This is arguably the most important section for TypeScript developers. + +### Option — replaces `T | null | undefined` + +🟦 **TypeScript** +```typescript +function findUser(id: number): User | undefined { + return users.find(u => u.id === id); +} + +const user = findUser(42); +if (user) { + console.log(user.name); +} +``` + +🦀 **Rust** +```rust +fn find_user(id: u64) -> Option { + users.iter().find(|u| u.id == id).cloned() +} + +// Pattern matching +match find_user(42) { + Some(user) => println!("{}", user.name), + None => println!("not found"), +} + +// Or more concisely: +if let Some(user) = find_user(42) { + println!("{}", user.name); +} +``` + +### Result — replaces try/catch + +🟦 **TypeScript** +```typescript +function parseConfig(path: string): Config { + const data = fs.readFileSync(path, "utf-8"); // might throw + return JSON.parse(data); // might throw +} +``` + +🦀 **Rust** +```rust +fn parse_config(path: &str) -> Result> { + let data = std::fs::read_to_string(path)?; // ? propagates error + let config: Config = serde_json::from_str(&data)?; + Ok(config) +} +``` + +💡 **Key insight**: The `?` operator is Rust's answer to `try/catch`. If the `Result` is `Ok`, +it unwraps the value. If it's `Err`, it returns the error from the current function immediately. +It's like writing `if (err) return err;` after every operation — but in one character. + +🏋️ **Exercise**: Create a `Student` struct with `name: String` and `grades: Vec`. Add a +method `average(&self) -> Option` that returns `None` if there are no grades. diff --git a/typescript-book/src/ch06-enums-and-pattern-matching.md b/typescript-book/src/ch06-enums-and-pattern-matching.md new file mode 100644 index 0000000..d67cea3 --- /dev/null +++ b/typescript-book/src/ch06-enums-and-pattern-matching.md @@ -0,0 +1,206 @@ +# Enums and Pattern Matching + +## TypeScript Discriminated Unions → Rust Enums + +This chapter will feel familiar. TypeScript's discriminated unions and Rust's enums solve the +same problem — representing a value that can be one of several distinct variants — but Rust +enums are more powerful because the compiler fully understands them. + +🟦 **TypeScript** +```typescript +type Shape = + | { kind: "circle"; radius: number } + | { kind: "rectangle"; width: number; height: number } + | { kind: "point" }; + +function area(shape: Shape): number { + switch (shape.kind) { + case "circle": + return Math.PI * shape.radius ** 2; + case "rectangle": + return shape.width * shape.height; + case "point": + return 0; + } +} +``` + +🦀 **Rust** +```rust +enum Shape { + Circle { radius: f64 }, + Rectangle { width: f64, height: f64 }, + Point, +} + +fn area(shape: &Shape) -> f64 { + match shape { + Shape::Circle { radius } => std::f64::consts::PI * radius * radius, + Shape::Rectangle { width, height } => width * height, + Shape::Point => 0.0, + } +} +``` + +💡 **Key insight**: Rust enums are *algebraic data types* (tagged unions). Each variant can +hold different data. The `match` is exhaustive — if you add a new variant, every `match` in +your codebase will produce a compile error until you handle it. TypeScript can approximate +this with `never` exhaustiveness checks, but Rust enforces it natively. + +## Enum Variants + +Rust enums support three kinds of variants: + +```rust +enum Message { + Quit, // unit variant (no data) + Echo(String), // tuple variant (unnamed fields) + Move { x: i32, y: i32 }, // struct variant (named fields) + Color(u8, u8, u8), // tuple variant with multiple fields +} +``` + +## Methods on Enums + +Just like structs, enums can have `impl` blocks: + +```rust +impl Message { + fn is_quit(&self) -> bool { + matches!(self, Message::Quit) + } + + fn describe(&self) -> String { + match self { + Message::Quit => "quit".to_string(), + Message::Echo(text) => format!("echo: {text}"), + Message::Move { x, y } => format!("move to ({x}, {y})"), + Message::Color(r, g, b) => format!("color: ({r}, {g}, {b})"), + } + } +} +``` + +## Option and Result Are Just Enums + +This is a crucial realization. They're not special language features — they're regular enums +defined in the standard library: + +```rust +// Simplified definitions: +enum Option { + Some(T), + None, +} + +enum Result { + Ok(T), + Err(E), +} +``` + +Because they're enums, everything you learn about pattern matching applies to them. + +## Advanced Pattern Matching + +### Guards + +```rust +match temperature { + t if t < 0 => println!("freezing"), + 0..=20 => println!("cold"), + 21..=30 => println!("comfortable"), + _ => println!("hot"), +} +``` + +### Or-patterns + +```rust +match status_code { + 200 | 201 | 204 => println!("success"), + 301 | 302 => println!("redirect"), + 400..=499 => println!("client error"), + 500..=599 => println!("server error"), + _ => println!("unknown"), +} +``` + +### Nested Destructuring + +```rust +struct Point { x: f64, y: f64 } + +enum PlotItem { + Marker(Point), + Line(Point, Point), +} + +match item { + PlotItem::Marker(Point { x, y }) => { + println!("marker at ({x}, {y})"); + } + PlotItem::Line(Point { x: x1, y: y1 }, Point { x: x2, y: y2 }) => { + println!("line from ({x1}, {y1}) to ({x2}, {y2})"); + } +} +``` + +### Binding with @ + +```rust +match response.status { + status @ 200..=299 => println!("success: {status}"), + status @ 400..=499 => println!("client error: {status}"), + status => println!("other: {status}"), +} +``` + +## C-like Enums + +When you just need named constants (like TypeScript's `enum`): + +🟦 **TypeScript** +```typescript +enum Direction { + North, + South, + East, + West, +} +``` + +🦀 **Rust** +```rust +#[derive(Debug, Clone, Copy, PartialEq)] +enum Direction { + North, + South, + East, + West, +} +``` + +These can also have explicit discriminant values: + +```rust +#[repr(u8)] +enum HttpMethod { + Get = 0, + Post = 1, + Put = 2, + Delete = 3, +} +``` + +## The `matches!` Macro + +A concise way to check if a value matches a pattern without a full `match` block: + +```rust +let is_vowel = matches!(ch, 'a' | 'e' | 'i' | 'o' | 'u'); +``` + +🏋️ **Exercise**: Define an `HttpResponse` enum with variants `Ok(String)` (body), +`Redirect(String)` (url), `NotFound`, and `Error(u16, String)` (status code, message). +Write a function that returns appropriate log messages for each variant. diff --git a/typescript-book/src/ch07-1-lifetimes.md b/typescript-book/src/ch07-1-lifetimes.md new file mode 100644 index 0000000..9c6c5e2 --- /dev/null +++ b/typescript-book/src/ch07-1-lifetimes.md @@ -0,0 +1,113 @@ +# Lifetimes for TypeScript Developers + +## Why Lifetimes Exist + +In TypeScript, the garbage collector knows when to free memory by counting references at +runtime. In Rust, the compiler must prove at *compile time* that every reference is valid. +Lifetimes are the annotations that help the compiler do this. + +## The Problem + +```rust +fn longest(a: &str, b: &str) -> &str { + if a.len() >= b.len() { a } else { b } +} +``` + +This won't compile. The compiler asks: *"The return type is a reference — but a reference to +what? Does the returned reference live as long as `a` or `b`?"* It needs help. + +## Lifetime Annotations + +```rust +fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { + if a.len() >= b.len() { a } else { b } +} +``` + +`'a` is a *lifetime parameter*. It says: "the returned reference lives at least as long as +the shorter of the two input lifetimes." The compiler uses this to verify callers don't use the +result after either input is dropped. + +💡 **Key insight**: Lifetime annotations don't change how long values live. They describe +relationships between references so the compiler can verify correctness. They're like +TypeScript type annotations — they don't change runtime behavior, just help the type checker. + +## Lifetime Elision Rules + +Most of the time, you don't need to write lifetime annotations. The compiler applies three +elision rules automatically: + +1. Each reference parameter gets its own lifetime: `fn foo(x: &str, y: &str)` becomes + `fn foo<'a, 'b>(x: &'a str, y: &'b str)`. +2. If there's exactly one input lifetime, it's applied to all output references: + `fn foo(x: &str) -> &str` becomes `fn foo<'a>(x: &'a str) -> &'a str`. +3. If one of the parameters is `&self` or `&mut self`, its lifetime is applied to all + output references. + +These rules cover ~95% of cases. You only write explicit lifetimes when the compiler can't +figure it out — typically when you have multiple reference inputs and a reference output. + +## Lifetimes in Structs + +If a struct holds a reference, it needs a lifetime annotation: + +```rust +struct Excerpt<'a> { + text: &'a str, +} + +let novel = String::from("Call me Ishmael. Some years ago..."); +let first_sentence = novel.split('.').next().unwrap(); +let excerpt = Excerpt { text: first_sentence }; +// excerpt cannot outlive novel, because it borrows from it +``` + +🟦 **TypeScript analogy**: Imagine if TypeScript enforced that an object holding a reference +to another object's data could never outlive that object — no dangling references, guaranteed +at compile time. + +## The `'static` Lifetime + +`'static` means "lives for the entire duration of the program." String literals have this +lifetime: + +```rust +let s: &'static str = "I live forever"; +``` + +⚠️ **Pitfall**: Seeing `'static` in error messages usually means the compiler wants you to own +the data rather than borrow it. The fix is often changing `&str` to `String`. + +## Common Patterns + +### Returning Owned Data Avoids Lifetime Issues + +When in doubt, return an owned type: + +```rust +// Simple: no lifetime needed +fn greet(name: &str) -> String { + format!("Hello, {name}!") +} +``` + +### Storing References vs Owned Data + +```rust +// This struct borrows — needs a lifetime +struct BorrowedConfig<'a> { + name: &'a str, +} + +// This struct owns — no lifetime needed +struct OwnedConfig { + name: String, +} +``` + +For TypeScript developers, start with owned data (`String`, `Vec`) everywhere. Optimize to +borrowed references later when profiling shows it matters. + +🏋️ **Exercise**: Write a struct `Highlight<'a>` that holds a `&'a str` reference to a portion +of text and a `color: &'a str`. Write a function that creates a `Highlight` from a text string. diff --git a/typescript-book/src/ch07-ownership-and-borrowing.md b/typescript-book/src/ch07-ownership-and-borrowing.md new file mode 100644 index 0000000..b0d12c6 --- /dev/null +++ b/typescript-book/src/ch07-ownership-and-borrowing.md @@ -0,0 +1,181 @@ +# Ownership and Borrowing + +This is the chapter. If you absorb one concept from this entire book, let it be ownership. +It's the idea that has no equivalent in TypeScript, and it's the reason Rust can guarantee +memory safety without a garbage collector. + +## The Problem Ownership Solves + +In TypeScript, the V8 garbage collector tracks every object and frees it when nothing +references it. This is convenient but has costs: GC pauses, unpredictable latency, and +higher memory usage. Rust replaces the garbage collector with three compile-time rules. + +## The Three Rules + +1. **Every value has exactly one owner** (a variable binding). +2. **When the owner goes out of scope, the value is dropped** (freed). +3. **At any given time, you can have *either* one mutable reference *or* any number of + immutable references** — but not both. + +That's it. These three rules, enforced at compile time, eliminate use-after-free, double-free, +data races, and dangling pointers. + +## Move Semantics + +🟦 **TypeScript** — assignment copies the reference, both variables point to the same object: +```typescript +const a = { name: "Alice" }; +const b = a; // b and a both reference the same object +console.log(a); // ✅ works fine +``` + +🦀 **Rust** — assignment *moves* the value; the original is invalidated: +```rust +let a = String::from("Alice"); +let b = a; // value moves from a to b +// println!("{a}"); // ❌ compile error: value used after move +println!("{b}"); // ✅ b owns the string now +``` + +💡 **Key insight**: A move is not a copy and not a reference — it's a transfer of ownership. +After `let b = a;`, the variable `a` no longer exists conceptually. The compiler enforces this. + +### Copy Types + +Small, stack-allocated types implement the `Copy` trait and are *copied* instead of moved: + +```rust +let x: i32 = 42; +let y = x; // x is copied (integers are Copy) +println!("{x}"); // ✅ still valid +``` + +Types that are `Copy`: all integers, floats, `bool`, `char`, tuples of `Copy` types, arrays +of `Copy` types. Types that are *not* `Copy`: `String`, `Vec`, `HashMap`, any type +that manages heap memory. + +### Clone — Explicit Deep Copy + +When you genuinely need a copy of a non-`Copy` type: + +```rust +let a = String::from("hello"); +let b = a.clone(); // explicit deep copy +println!("{a}"); // ✅ a is still valid +println!("{b}"); // ✅ b is an independent copy +``` + +## Borrowing — References + +Most of the time, you don't want to transfer ownership — you just want to *look at* a value +or temporarily modify it. This is borrowing. + +### Immutable References (`&T`) + +```rust +fn calculate_length(s: &String) -> usize { + s.len() + // s goes out of scope here, but since it's a reference, + // the value it refers to is NOT dropped +} + +let name = String::from("Alice"); +let len = calculate_length(&name); // borrow name +println!("{name} is {len} bytes"); // ✅ name still valid +``` + +🟦 **TypeScript analogy**: This is like passing a `Readonly` reference — you can read but +not modify. Except Rust enforces it at compile time, not just as a type hint. + +### Mutable References (`&mut T`) + +```rust +fn add_greeting(s: &mut String) { + s.push_str(", hello!"); +} + +let mut name = String::from("Alice"); +add_greeting(&mut name); +println!("{name}"); // "Alice, hello!" +``` + +### The Borrow Rules Visualized + +``` + ┌──────────────────────────────────┐ + │ At any point in time, you have: │ + │ │ + │ EITHER many &T (readers) │ + │ OR one &mut T (writer) │ + │ │ + │ NEVER both at the same time │ + └──────────────────────────────────┘ +``` + +```rust +let mut data = vec![1, 2, 3]; + +let r1 = &data; // ✅ first immutable borrow +let r2 = &data; // ✅ second immutable borrow +println!("{r1:?} {r2:?}"); + +let r3 = &mut data; // ✅ mutable borrow (r1 and r2 are no longer used) +r3.push(4); +``` + +⚠️ **Common pitfall**: The borrow checker uses *non-lexical lifetimes* (NLL). A borrow lasts +until its last use, not until the end of the scope. So this works: + +```rust +let mut v = vec![1, 2, 3]; +let first = &v[0]; +println!("{first}"); // last use of `first` +v.push(4); // ✅ mutable borrow starts after immutable one ends +``` + +But this doesn't: +```rust +let mut v = vec![1, 2, 3]; +let first = &v[0]; +v.push(4); // ❌ mutable borrow while immutable borrow is active +println!("{first}"); // first is used AFTER the push +``` + +## Ownership in Functions + +```rust +fn take_ownership(s: String) { + println!("{s}"); +} // s is dropped here + +fn borrow(s: &String) { + println!("{s}"); +} // s goes out of scope but the value is NOT dropped + +let name = String::from("Alice"); + +borrow(&name); // borrow — name still valid +take_ownership(name); // move — name is consumed +// println!("{name}"); // ❌ name was moved +``` + +## Returning Ownership + +```rust +fn create_greeting(name: &str) -> String { + format!("Hello, {name}!") // a new String is created and returned (moved to caller) +} + +let greeting = create_greeting("Alice"); // caller now owns this String +``` + +## Thinking Like the Borrow Checker + +When you get a borrow checker error, ask yourself: +1. **Who owns this value?** Trace back to the `let` binding. +2. **Is someone else still reading it?** Check for active `&T` references. +3. **Am I trying to mutate while someone reads?** That's the most common conflict. +4. **Can I restructure to use the borrow, then release it, then mutate?** + +🏋️ **Exercise**: Write a function `longest(a: &str, b: &str) -> &str` that returns the longer +string. You'll discover you need a *lifetime annotation* — that's the topic of the next section. diff --git a/typescript-book/src/ch08-1-testing-patterns.md b/typescript-book/src/ch08-1-testing-patterns.md new file mode 100644 index 0000000..a9f45c9 --- /dev/null +++ b/typescript-book/src/ch08-1-testing-patterns.md @@ -0,0 +1,116 @@ +# Testing Patterns + +## Built-in Test Framework + +Unlike TypeScript (where you pick jest, vitest, mocha, etc.), Rust has testing built into +`cargo`. No installation, no configuration. + +🟦 **TypeScript (vitest)** +```typescript +import { describe, it, expect } from "vitest"; +import { add } from "./math"; + +describe("add", () => { + it("adds two numbers", () => { + expect(add(2, 3)).toBe(5); + }); +}); +``` + +🦀 **Rust** +```rust +fn add(a: i32, b: i32) -> i32 { + a + b +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn adds_two_numbers() { + assert_eq!(add(2, 3), 5); + } +} +``` + +Run with: `cargo test` + +## Test Assertions + +| vitest / jest | Rust | Purpose | +|--------------|------|---------| +| `expect(x).toBe(y)` | `assert_eq!(x, y)` | Equality | +| `expect(x).not.toBe(y)` | `assert_ne!(x, y)` | Inequality | +| `expect(condition).toBeTruthy()` | `assert!(condition)` | Boolean check | +| Custom message | `assert!(cond, "msg: {}", val)` | With context | + +## Testing for Errors + +🟦 **TypeScript** +```typescript +expect(() => parsePort("abc")).toThrow(); +``` + +🦀 **Rust** +```rust +#[test] +fn parse_invalid_port() { + let result = parse_port("abc"); + assert!(result.is_err()); +} + +#[test] +#[should_panic(expected = "out of bounds")] +fn panics_on_invalid_index() { + let v = vec![1, 2, 3]; + let _ = v[99]; +} +``` + +## Integration Tests + +Place files in a `tests/` directory at the project root: + +``` +my-project/ +├── src/ +│ └── lib.rs +├── tests/ +│ └── integration_test.rs +└── Cargo.toml +``` + +```rust +// tests/integration_test.rs +use my_project::add; + +#[test] +fn integration_add() { + assert_eq!(add(10, 20), 30); +} +``` + +Each file in `tests/` is compiled as a separate crate, so it only sees your public API. + +## Doc Tests + +Rust can test code examples in documentation comments: + +```rust +/// Adds two numbers. +/// +/// # Examples +/// +/// ``` +/// assert_eq!(my_crate::add(2, 3), 5); +/// ``` +pub fn add(a: i32, b: i32) -> i32 { + a + b +} +``` + +`cargo test` runs these doc examples as tests. This ensures your documentation never goes stale. + +🏋️ **Exercise**: Write a `divide(a: f64, b: f64) -> Result` function with tests +for the happy path, division by zero, and a doc test. diff --git a/typescript-book/src/ch08-crates-and-modules.md b/typescript-book/src/ch08-crates-and-modules.md new file mode 100644 index 0000000..dd09db5 --- /dev/null +++ b/typescript-book/src/ch08-crates-and-modules.md @@ -0,0 +1,182 @@ +# Crates and Modules + +## Module System Overview + +| TypeScript | Rust | Purpose | +|-----------|------|---------| +| File = module | `mod` declaration | Namespace boundary | +| `export` | `pub` | Make items visible | +| `import { X } from` | `use path::X` | Bring items into scope | +| `package.json` | `Cargo.toml` | Project manifest | +| npm package | Crate | Unit of compilation and distribution | + +## Defining Modules + +🟦 **TypeScript** — each file is a module, exports opt in: +```typescript +// utils.ts +export function add(a: number, b: number): number { + return a + b; +} + +// main.ts +import { add } from "./utils"; +``` + +🦀 **Rust** — modules are declared explicitly: + +```rust +// src/main.rs +mod utils; + +fn main() { + println!("{}", utils::add(2, 3)); +} + +// src/utils.rs +pub fn add(a: i32, b: i32) -> i32 { + a + b +} +``` + +💡 **Key insight**: In Rust, `mod utils;` tells the compiler to look for `src/utils.rs` or +`src/utils/mod.rs`. Nothing is exported by default — you must add `pub` to every function, +struct, and field you want visible. + +## Visibility + +| Rust keyword | TypeScript equivalent | Meaning | +|-------------|----------------------|---------| +| (nothing) | (nothing — not exported) | Private to current module | +| `pub` | `export` | Public to everyone | +| `pub(crate)` | (no equivalent) | Public within the crate only | +| `pub(super)` | (no equivalent) | Public to parent module | + +```rust +pub struct User { + pub name: String, // public + pub(crate) email: String, // visible within the crate + password_hash: String, // private (no pub) +} +``` + +⚠️ **Pitfall**: In TypeScript, if you export a class, all its properties are accessible. In +Rust, a `pub struct` with private fields cannot be constructed outside its module — you need a +constructor function. + +```rust +impl User { + pub fn new(name: String, email: String, password: &str) -> Self { + Self { + name, + email, + password_hash: hash(password), + } + } +} +``` + +## Nested Modules + +```rust +// src/lib.rs +pub mod api { + pub mod handlers { + pub fn health_check() -> &'static str { "ok" } + } + pub mod middleware { + pub fn log_request() { /* ... */ } + } +} + +// Usage: +use crate::api::handlers::health_check; +``` + +## File Layout + +For a module `api` with submodules: + +``` +src/ +├── main.rs // mod api; +└── api/ + ├── mod.rs // pub mod handlers; pub mod middleware; + ├── handlers.rs + └── middleware.rs +``` + +Or with the newer convention (Rust 2018+): + +``` +src/ +├── main.rs // mod api; +├── api.rs // pub mod handlers; pub mod middleware; +└── api/ + ├── handlers.rs + └── middleware.rs +``` + +## `use` Declarations + +```rust +// Bring a single item into scope +use std::collections::HashMap; + +// Bring multiple items +use std::collections::{HashMap, HashSet, BTreeMap}; + +// Rename on import (like TypeScript's `import { X as Y }`) +use std::collections::HashMap as Map; + +// Glob import (generally discouraged, like `import *`) +use std::collections::*; + +// Re-export (like TypeScript's `export { X } from "./module"`) +pub use self::handlers::health_check; +``` + +## Crates — Library vs Binary + +A *crate* is Rust's compilation unit. There are two kinds: + +- **Binary crate** — has a `main()` function, produces an executable. Entry: `src/main.rs`. +- **Library crate** — no `main()`, produces a `.rlib`. Entry: `src/lib.rs`. + +A single Cargo project can have both `src/main.rs` and `src/lib.rs`. + +## Adding Dependencies + +```toml +# Cargo.toml +[dependencies] +serde = { version = "1", features = ["derive"] } +reqwest = { version = "0.12", features = ["json"] } +tokio = { version = "1", features = ["full"] } +``` + +Or via the command line: +```bash +cargo add serde --features derive +cargo add tokio --features full +``` + +## Workspaces — Monorepos + +Similar to npm workspaces or Turborepo: + +```toml +# Cargo.toml (root) +[workspace] +members = [ + "api-server", + "shared-types", + "cli-tool", +] +``` + +Each member is a separate crate with its own `Cargo.toml` but they share a single +`target/` directory and `Cargo.lock`. + +🏋️ **Exercise**: Create a library crate with a `math` module containing `add`, `subtract`, +and `multiply` functions. Write a binary crate that depends on the library and uses all three. diff --git a/typescript-book/src/ch09-1-error-handling-best-practices.md b/typescript-book/src/ch09-1-error-handling-best-practices.md new file mode 100644 index 0000000..9d6a2f6 --- /dev/null +++ b/typescript-book/src/ch09-1-error-handling-best-practices.md @@ -0,0 +1,88 @@ +# Error Handling Best Practices + +## Library vs Application Errors + +### Libraries: Use typed errors with `thiserror` + +```rust +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum DatabaseError { + #[error("connection failed: {0}")] + Connection(String), + #[error("query failed: {0}")] + Query(String), + #[error("record not found: {table}/{id}")] + NotFound { table: String, id: String }, +} +``` + +### Applications: Use `anyhow` for convenience + +```rust +use anyhow::{bail, ensure, Context, Result}; + +fn run() -> Result<()> { + let port: u16 = std::env::var("PORT") + .context("PORT env var not set")? + .parse() + .context("PORT must be a valid number")?; + + ensure!(port > 0, "PORT must be positive, got {port}"); + + if port == 80 { + bail!("port 80 requires root privileges"); + } + + Ok(()) +} +``` + +## Error Context Chains + +`anyhow` provides `.context()` which wraps errors with additional information, creating a +chain similar to JavaScript's `Error.cause`: + +```rust +std::fs::read_to_string("config.toml") + .context("failed to read config file") + .context("during application startup")?; + +// Error output: +// during application startup +// Caused by: +// 0: failed to read config file +// 1: No such file or directory (os error 2) +``` + +## The `?` in `main()` + +```rust +fn main() -> Result<()> { + let config = load_config("app.toml")?; + start_server(config)?; + Ok(()) +} +``` + +When `main()` returns `Result`, Rust prints the error and exits with code 1 on `Err`. + +## Mapping Errors + +```rust +// Map error type +let count: i32 = input.parse() + .map_err(|_| AppError::InvalidInput("expected a number".into()))?; + +// Map to Option +let maybe: Option = input.parse().ok(); + +// Map from Option to Result +let value = maybe_value.ok_or(AppError::MissingField("name"))?; +let value = maybe_value.ok_or_else(|| expensive_error())?; +``` + +🏋️ **Exercise**: Define a custom error enum for a URL shortener service with variants for +invalid URLs, duplicate slugs, and database errors. Use `thiserror` for the library and +`anyhow` in the application layer. diff --git a/typescript-book/src/ch09-error-handling.md b/typescript-book/src/ch09-error-handling.md new file mode 100644 index 0000000..fbdac42 --- /dev/null +++ b/typescript-book/src/ch09-error-handling.md @@ -0,0 +1,167 @@ +# Error Handling + +## Philosophy: Errors Are Values, Not Exceptions + +In TypeScript, errors are *thrown* and *caught*. Any function can throw at any time, and there's +no way to know from the signature whether it will. In Rust, errors are *returned* as values. +The function signature tells you exactly what can go wrong. + +🟦 **TypeScript** — error path is invisible: +```typescript +function readConfig(path: string): Config { + const data = fs.readFileSync(path, "utf-8"); // might throw + return JSON.parse(data); // might also throw +} +``` + +🦀 **Rust** — error path is explicit: +```rust +fn read_config(path: &str) -> Result> { + let data = fs::read_to_string(path)?; + let config: Config = serde_json::from_str(&data)?; + Ok(config) +} +``` + +## `Result` + +```rust +enum Result { + Ok(T), + Err(E), +} +``` + +### Working with Result + +```rust +let result: Result = "42".parse::() + .map_err(|e| e.to_string()); + +// Pattern matching +match result { + Ok(n) => println!("parsed: {n}"), + Err(e) => println!("error: {e}"), +} + +// Combinator methods (similar to Promise chaining) +let doubled = "42".parse::() + .map(|n| n * 2) // transform Ok value + .unwrap_or(0); // provide default on Err +``` + +## The `?` Operator — Rust's try/catch Replacement + +The `?` operator is the most important error-handling tool. It: +1. Unwraps `Ok(value)` and returns the value. +2. On `Err(e)`, converts the error (via `From`) and returns it from the current function. + +```rust +fn fetch_user_name(id: u64) -> Result> { + let response = reqwest::blocking::get(format!("/users/{id}"))?; + let user: User = response.json()?; + Ok(user.name) +} +``` + +This replaces the TypeScript pattern of: +```typescript +try { + const response = await fetch(`/users/${id}`); + const user = await response.json(); + return user.name; +} catch (e) { + throw new Error(`Failed to fetch user: ${e}`); +} +``` + +## `panic!` — The Unrecoverable Error + +`panic!` is for bugs, not expected errors. It's analogous to throwing an uncaught exception +that crashes the process. + +```rust +// Use panic for programming errors / invariant violations +fn first_element(v: &[i32]) -> i32 { + if v.is_empty() { + panic!("called first_element on an empty slice"); + } + v[0] +} +``` + +### When to panic vs return Result + +| Situation | Use | +|----------|-----| +| Invalid user input | `Result` | +| File not found | `Result` | +| Network failure | `Result` | +| Index out of bounds (your bug) | `panic!` | +| Invariant violation | `panic!` | +| Prototype / example code | `.unwrap()` (panics on Err) | + +## `unwrap()` and `expect()` + +```rust +// .unwrap() — panics with a generic message if Err +let port: u16 = env::var("PORT").unwrap().parse().unwrap(); + +// .expect() — panics with your message if Err +let port: u16 = env::var("PORT") + .expect("PORT must be set") + .parse() + .expect("PORT must be a valid number"); +``` + +💡 **Key insight**: In production code, avoid `.unwrap()`. Use `?` to propagate errors, or +`.expect("reason")` if you're certain the value exists and want a clear message if you're wrong. + +## Converting Between Error Types + +The `?` operator uses the `From` trait to convert between error types: + +```rust +use std::num::ParseIntError; + +#[derive(Debug)] +enum AppError { + Io(std::io::Error), + Parse(ParseIntError), +} + +impl From for AppError { + fn from(e: std::io::Error) -> Self { AppError::Io(e) } +} + +impl From for AppError { + fn from(e: ParseIntError) -> Self { AppError::Parse(e) } +} + +fn load_count(path: &str) -> Result { + let text = std::fs::read_to_string(path)?; // io::Error → AppError + let count = text.trim().parse::()?; // ParseIntError → AppError + Ok(count) +} +``` + +## Crates That Help + +- **`anyhow`** — for applications. Provides `anyhow::Result` that wraps any error. +- **`thiserror`** — for libraries. Derives `Error` + `Display` + `From` for custom error enums. + +```rust +// With anyhow (application code): +use anyhow::{Context, Result}; + +fn load_config(path: &str) -> Result { + let text = std::fs::read_to_string(path) + .context("failed to read config file")?; + let config = toml::from_str(&text) + .context("failed to parse config")?; + Ok(config) +} +``` + +🏋️ **Exercise**: Write a function that reads a file, parses each line as an integer, and +returns the sum. Use `?` for error propagation and provide meaningful error context. diff --git a/typescript-book/src/ch10-1-generics-deep-dive.md b/typescript-book/src/ch10-1-generics-deep-dive.md new file mode 100644 index 0000000..cb9fa57 --- /dev/null +++ b/typescript-book/src/ch10-1-generics-deep-dive.md @@ -0,0 +1,124 @@ +# Generics Deep Dive + +## Monomorphization — Generics at Zero Cost + +In TypeScript, generics are erased at compile time — `Array` and `Array` are +the same `Array` at runtime. In Rust, the compiler generates specialized code for each concrete +type used. This is called *monomorphization*. + +```rust +fn double + Copy>(x: T) -> T { + x * x +} + +// When you call: +double(3_i32); // compiler generates: fn double_i32(x: i32) -> i32 +double(2.5_f64); // compiler generates: fn double_f64(x: f64) -> f64 +``` + +This means generics in Rust have zero runtime overhead — they're as fast as hand-written +specialized functions. + +## Generic Structs + +```rust +struct Pair { + first: T, + second: U, +} + +impl Pair { + fn new(first: T, second: U) -> Self { + Pair { first, second } + } +} + +// Conditional implementations +impl Pair { + fn display(&self) { + println!("({}, {})", self.first, self.second); + } +} +``` + +## Generic Enums + +You've already seen the two most important generic enums: + +```rust +enum Option { Some(T), None } +enum Result { Ok(T), Err(E) } +``` + +## Associated Types vs Generic Parameters + +🟦 **TypeScript** — generic interface parameter: +```typescript +interface Iterator { + next(): T | undefined; +} +``` + +🦀 **Rust** — associated type: +```rust +trait Iterator { + type Item; // associated type + fn next(&mut self) -> Option; +} +``` + +Associated types are used when there's exactly one natural type per implementation. Generic +parameters are used when a type can implement the trait multiple times with different types: + +```rust +// Associated type: a Vec has ONE Item type +impl Iterator for MyIter { + type Item = i32; + fn next(&mut self) -> Option { /* ... */ } +} + +// Generic parameter: a type can implement From for MANY T's +impl From for MyType { /* ... */ } +impl From for MyType { /* ... */ } +``` + +## Phantom Types — Types Without Data + +A powerful pattern for encoding state in the type system: + +```rust +use std::marker::PhantomData; + +struct Draft; +struct Published; + +struct Article { + title: String, + body: String, + _state: PhantomData, +} + +impl Article { + fn new(title: String) -> Self { + Article { title, body: String::new(), _state: PhantomData } + } + + fn publish(self) -> Article { + Article { title: self.title, body: self.body, _state: PhantomData } + } +} + +impl Article { + fn url(&self) -> String { + format!("/articles/{}", self.title.to_lowercase().replace(' ', "-")) + } +} + +// draft.url() // ❌ compile error: method not found +// published.url() // ✅ works +``` + +This has no TypeScript equivalent — it's compile-time state tracking with zero runtime cost. + +🏋️ **Exercise**: Create a generic `Stack` with `push`, `pop`, and `peek` methods. Add a +`display` method that's only available when `T: Display`. diff --git a/typescript-book/src/ch10-traits-and-generics.md b/typescript-book/src/ch10-traits-and-generics.md new file mode 100644 index 0000000..ffa3208 --- /dev/null +++ b/typescript-book/src/ch10-traits-and-generics.md @@ -0,0 +1,173 @@ +# Traits and Generics + +## Traits — Rust's Interfaces + +Traits are Rust's mechanism for shared behavior. They're similar to TypeScript interfaces, but +with important differences: traits can provide default implementations, and they use *nominal* +typing (types must explicitly implement a trait) rather than *structural* typing. + +🟦 **TypeScript** — structural (duck) typing: +```typescript +interface Printable { + display(): string; +} + +// Any object with a display() method satisfies Printable — no declaration needed. +const item = { display: () => "hello" }; +function print(p: Printable) { console.log(p.display()); } +print(item); // ✅ works because the shape matches +``` + +🦀 **Rust** — nominal typing: +```rust +trait Printable { + fn display(&self) -> String; +} + +struct Item; + +impl Printable for Item { + fn display(&self) -> String { + "hello".to_string() + } +} + +fn print(p: &impl Printable) { + println!("{}", p.display()); +} +``` + +💡 **Key insight**: In TypeScript, compatibility is checked by shape. In Rust, a type must +explicitly `impl` a trait. This is more verbose but eliminates accidental matches and enables +the compiler to monomorphize generics. + +## Default Implementations + +```rust +trait Summary { + fn title(&self) -> &str; + + // Default implementation — types can override or keep it + fn summary(&self) -> String { + format!("{} (read more...)", self.title()) + } +} + +struct Article { title: String, body: String } + +impl Summary for Article { + fn title(&self) -> &str { &self.title } + // summary() uses the default implementation +} +``` + +## Common Standard Library Traits + +| Trait | TypeScript equivalent | Purpose | +|-------|---------------------|---------| +| `Display` | `toString()` | Human-readable formatting | +| `Debug` | `console.log` output | Developer-facing formatting | +| `Clone` | Spread / structuredClone | Deep copy | +| `Copy` | (primitive value semantics) | Implicit bitwise copy | +| `PartialEq` / `Eq` | `===` | Equality comparison | +| `PartialOrd` / `Ord` | `<`, `>`, compareFn | Ordering | +| `Default` | Default values | Provide a default value | +| `From` / `Into` | Type coercion / conversion | Type conversion | +| `Iterator` | `Symbol.iterator` | Iteration protocol | + +### Deriving Traits + +```rust +#[derive(Debug, Clone, PartialEq, Default)] +struct Config { + host: String, + port: u16, + debug: bool, +} +``` + +`#[derive(...)]` auto-implements traits when all fields support them. This is like TypeScript +automatically getting serialization — but explicit. + +## Trait Bounds — Constrained Generics + +🟦 **TypeScript** — generic constraints: +```typescript +function longest(a: T, b: T): T { + return a.length >= b.length ? a : b; +} +``` + +🦀 **Rust** — trait bounds: +```rust +fn longest>(a: T, b: T) -> T { + if a.as_ref().len() >= b.as_ref().len() { a } else { b } +} +``` + +### Multiple Bounds + +```rust +// Syntax 1: inline +fn process(item: T) { /* ... */ } + +// Syntax 2: where clause (cleaner for complex bounds) +fn process(item: T) +where + T: Clone + Debug + Display, +{ + /* ... */ +} +``` + +## `impl Trait` — Simplified Syntax + +```rust +// In function arguments (accepts any type implementing the trait): +fn notify(item: &impl Summary) { + println!("Breaking: {}", item.summary()); +} + +// In return position (returns some concrete type implementing the trait): +fn make_adder(x: i32) -> impl Fn(i32) -> i32 { + move |y| x + y +} +``` + +## Dynamic Dispatch — Trait Objects + +Sometimes you need to store different types implementing the same trait in a single collection. +This is like TypeScript's polymorphism: + +🟦 **TypeScript** +```typescript +interface Shape { area(): number; } +const shapes: Shape[] = [new Circle(5), new Rectangle(3, 4)]; +``` + +🦀 **Rust** +```rust +trait Shape { fn area(&self) -> f64; } + +let shapes: Vec> = vec![ + Box::new(Circle { radius: 5.0 }), + Box::new(Rectangle { width: 3.0, height: 4.0 }), +]; + +for shape in &shapes { + println!("{}", shape.area()); +} +``` + +`dyn Shape` is a *trait object* — it uses a vtable for runtime dispatch (like virtual methods +in C++). `Box` is a heap-allocated trait object. + +| | Static dispatch (`impl Trait` / generics) | Dynamic dispatch (`dyn Trait`) | +|--|---|---| +| Performance | Monomorphized, inlined | vtable lookup | +| Binary size | Larger (code duplicated per type) | Smaller | +| Flexibility | Type known at compile time | Type erased at runtime | + +🏋️ **Exercise**: Define a `Renderable` trait with a `render(&self) -> String` method. +Implement it for `Paragraph`, `Heading`, and `Image` structs. Create a `Vec>` +and render all items. diff --git a/typescript-book/src/ch11-from-and-into-traits.md b/typescript-book/src/ch11-from-and-into-traits.md new file mode 100644 index 0000000..3c79fde --- /dev/null +++ b/typescript-book/src/ch11-from-and-into-traits.md @@ -0,0 +1,117 @@ +# From and Into Traits + +## Type Conversions in Rust + +TypeScript has implicit coercion (`"5" + 3 === "53"`) and explicit conversion (`Number("5")`). +Rust has no implicit coercion — all conversions are explicit, and the `From`/`Into` traits are +the idiomatic way to do them. + +## `From` — Construct Self from T + +```rust +// String from &str +let s: String = String::from("hello"); + +// i64 from i32 (infallible widening) +let big: i64 = i64::from(42_i32); + +// Vec from &str +let bytes: Vec = Vec::from("hello"); +``` + +### Implementing From for Your Types + +```rust +struct Email(String); + +impl From for Email { + fn from(s: String) -> Self { + Email(s) + } +} + +impl From<&str> for Email { + fn from(s: &str) -> Self { + Email(s.to_string()) + } +} + +let email = Email::from("alice@example.com"); +``` + +## `Into` — Convert Self into T + +`Into` is the reciprocal of `From`. When you implement `From for B`, you automatically get +`Into for A`. Use `Into` in function signatures to accept flexible input: + +```rust +fn send_email(to: impl Into, body: &str) { + let email: Email = to.into(); + // ... +} + +send_email("alice@example.com", "Hello!"); // &str → Email +send_email(String::from("bob@b.com"), "Hi!"); // String → Email +``` + +## `TryFrom` / `TryInto` — Fallible Conversions + +When conversion can fail, use the `Try` variants: + +```rust +use std::num::TryFromIntError; + +struct Port(u16); + +impl TryFrom for Port { + type Error = String; + + fn try_from(value: i32) -> Result { + if value < 0 || value > 65535 { + Err(format!("port out of range: {value}")) + } else { + Ok(Port(value as u16)) + } + } +} + +let port = Port::try_from(8080)?; // Ok(Port(8080)) +let bad = Port::try_from(-1); // Err("port out of range: -1") +``` + +## `AsRef` and `AsMut` — Cheap Reference Conversions + +Used for functions that accept either owned or borrowed types: + +```rust +fn read_file(path: impl AsRef) -> std::io::Result { + std::fs::read_to_string(path) +} + +read_file("config.toml"); // &str +read_file(String::from("config.toml")); // String +read_file(std::path::PathBuf::from("config.toml")); // PathBuf +``` + +## `ToString` and `Display` + +Implement `Display` to get `ToString` for free: + +```rust +use std::fmt; + +struct Temperature(f64); + +impl fmt::Display for Temperature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:.1}°C", self.0) + } +} + +let t = Temperature(36.6); +println!("{t}"); // "36.6°C" +let s: String = t.to_string(); // also "36.6°C" +``` + +🏋️ **Exercise**: Create a `Celsius` and `Fahrenheit` struct. Implement `From` for +`Fahrenheit` and vice versa. Use the formula: F = C × 9/5 + 32. diff --git a/typescript-book/src/ch12-closures-and-iterators.md b/typescript-book/src/ch12-closures-and-iterators.md new file mode 100644 index 0000000..476325b --- /dev/null +++ b/typescript-book/src/ch12-closures-and-iterators.md @@ -0,0 +1,186 @@ +# Closures and Iterators + +## Closures + +Closures in Rust are anonymous functions that capture their environment — similar to arrow +functions in TypeScript. + +🟦 **TypeScript** +```typescript +const add = (a: number, b: number): number => a + b; +const numbers = [3, 1, 4, 1, 5]; +const doubled = numbers.map(n => n * 2); +``` + +🦀 **Rust** +```rust +let add = |a: i32, b: i32| -> i32 { a + b }; +let numbers = vec![3, 1, 4, 1, 5]; +let doubled: Vec = numbers.iter().map(|n| n * 2).collect(); +``` + +### Closure Syntax Variations + +```rust +let verbose = |x: i32| -> i32 { x + 1 }; // fully annotated +let inferred = |x| x + 1; // types inferred from usage +let multiline = |x| { + let y = x * 2; + y + 1 +}; +``` + +### Capturing — The Key Difference + +In TypeScript, closures always capture by reference. In Rust, closures can capture in three +ways, which map to three traits: + +| Trait | Capture mode | TypeScript analogy | +|-------|-------------|-------------------| +| `Fn` | Immutable borrow (`&T`) | Reading a const | +| `FnMut` | Mutable borrow (`&mut T`) | Modifying a `let` variable | +| `FnOnce` | Move (takes ownership) | N/A | + +```rust +let name = String::from("Alice"); + +// Fn — borrows name immutably +let greet = || println!("Hello, {name}"); +greet(); +greet(); // ✅ can call multiple times +println!("{name}"); // ✅ name still accessible + +// FnMut — borrows count mutably +let mut count = 0; +let mut increment = || { count += 1; }; +increment(); +increment(); + +// FnOnce — moves name into the closure +let consume = move || println!("Consumed: {name}"); +consume(); +// println!("{name}"); // ❌ name was moved +``` + +💡 **Key insight**: The compiler infers which trait a closure implements based on how it uses +captured variables. `move` forces ownership transfer regardless. + +## Iterators + +Rust iterators are lazy chains — like RxJS or lodash chains — that compile down to the same +machine code as hand-written loops. + +### The Iterator Trait + +```rust +trait Iterator { + type Item; + fn next(&mut self) -> Option; +} +``` + +### Creating Iterators + +```rust +let v = vec![1, 2, 3]; + +v.iter() // yields &i32 (borrows) +v.iter_mut() // yields &mut i32 (mutable borrows) +v.into_iter() // yields i32 (consumes the vec) +``` + +### Iterator Adapters (Lazy) + +These transform an iterator without consuming it: + +```rust +let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + +let result: Vec = numbers.iter() + .filter(|&&n| n % 2 == 0) // keep even numbers + .map(|&n| n * n) // square them + .take(3) // first 3 results + .collect(); // materialize + +// result = [4, 16, 36] +``` + +### TypeScript ↔ Rust Iterator Method Map + +| TypeScript (Array) | Rust (Iterator) | Notes | +|-------------------|-----------------|-------| +| `.map(fn)` | `.map(fn)` | Nearly identical | +| `.filter(fn)` | `.filter(fn)` | Closure takes `&&T` for `.iter()` | +| `.reduce(fn, init)` | `.fold(init, fn)` | Argument order swapped | +| `.find(fn)` | `.find(fn)` | Returns `Option<&T>` | +| `.some(fn)` | `.any(fn)` | | +| `.every(fn)` | `.all(fn)` | | +| `.flat()` | `.flatten()` | | +| `.flatMap(fn)` | `.flat_map(fn)` | | +| `.forEach(fn)` | `.for_each(fn)` | Prefer `for` loop instead | +| `.slice(0, n)` | `.take(n)` | Lazy | +| `.slice(n)` | `.skip(n)` | Lazy | +| `[...a, ...b]` | `a.chain(b)` | Lazy concatenation | +| `.entries()` | `.enumerate()` | Yields `(index, value)` | +| `.includes(x)` | `.any(|&v| v == x)` | Or use `.contains()` on slices | +| `Array.from({length: n}, (_, i) => i)` | `(0..n)` | Range | + +### Collecting Results + +`.collect()` is the terminator that materializes an iterator into a collection: + +```rust +let names: Vec = data.iter().map(|d| d.name.clone()).collect(); +let set: HashSet = numbers.iter().copied().collect(); +let map: HashMap<&str, i32> = pairs.into_iter().collect(); + +// Collect Results: if any item is Err, the whole thing is Err +let results: Result, _> = strings.iter() + .map(|s| s.parse::()) + .collect(); +``` + +### Method Chaining — A Complete Example + +🟦 **TypeScript** +```typescript +const topAuthors = posts + .filter(p => p.published) + .map(p => p.author) + .reduce((acc, author) => { + acc.set(author, (acc.get(author) ?? 0) + 1); + return acc; + }, new Map()); +``` + +🦀 **Rust** +```rust +let top_authors: HashMap<&str, usize> = posts.iter() + .filter(|p| p.published) + .map(|p| p.author.as_str()) + .fold(HashMap::new(), |mut acc, author| { + *acc.entry(author).or_insert(0) += 1; + acc + }); +``` + +### Zero-Cost Abstraction + +The iterator chain above compiles to roughly the same machine code as: + +```rust +let mut top_authors = HashMap::new(); +for post in &posts { + if post.published { + *top_authors.entry(post.author.as_str()).or_insert(0) += 1; + } +} +``` + +There is no intermediate allocation for `.filter()` or `.map()`. This is what "zero-cost +abstraction" means. + +🏋️ **Exercise**: Given a `Vec` of lines from a log file, use iterator chains to: +1. Filter lines containing "ERROR". +2. Extract the timestamp (first 19 characters). +3. Collect into a `Vec<&str>`. diff --git a/typescript-book/src/ch13-1-async-await.md b/typescript-book/src/ch13-1-async-await.md new file mode 100644 index 0000000..1db0b31 --- /dev/null +++ b/typescript-book/src/ch13-1-async-await.md @@ -0,0 +1,181 @@ +# Async/Await — From Promises to Futures + +## The Core Difference + +TypeScript's `async`/`await` and Rust's `async`/`await` look almost identical but work +fundamentally differently. + +| | TypeScript | Rust | +|--|-----------|------| +| Async primitive | `Promise` | `Future` | +| Runtime | Built into V8/Deno/Bun | You choose: `tokio`, `async-std`, `smol` | +| Execution | Eager — calling an `async` function starts it | Lazy — calling an `async` function returns an inert Future | +| Concurrency | Single-threaded event loop | Multi-threaded by default (tokio) | +| Cancellation | Not built in (AbortController is opt-in) | Dropping a Future cancels it | + +## Side-by-Side + +🟦 **TypeScript** +```typescript +async function fetchUser(id: number): Promise { + const response = await fetch(`/api/users/${id}`); + const user = await response.json(); + return user; +} +``` + +🦀 **Rust** (with `tokio` + `reqwest`) +```rust +async fn fetch_user(id: u64) -> Result { + let user: User = reqwest::get(format!("/api/users/{id}")) + .await? + .json() + .await?; + Ok(user) +} +``` + +## Setting Up an Async Runtime + +Unlike TypeScript where the runtime is always there, Rust needs you to start one: + +```toml +# Cargo.toml +[dependencies] +tokio = { version = "1", features = ["full"] } +``` + +```rust +#[tokio::main] +async fn main() { + let result = fetch_user(42).await; + println!("{result:?}"); +} +``` + +`#[tokio::main]` is a macro that wraps your `main` in a tokio runtime. It expands roughly to: + +```rust +fn main() { + tokio::runtime::Runtime::new().unwrap().block_on(async { + let result = fetch_user(42).await; + println!("{result:?}"); + }); +} +``` + +## Futures Are Lazy + +💡 **This is the most important difference.** In TypeScript, calling an `async` function +immediately starts executing it. In Rust, it only returns a `Future` — nothing runs until +you `.await` or spawn it. + +```rust +let future = fetch_user(42); // nothing happens yet! +let user = future.await; // NOW it runs +``` + +## Concurrent Execution + +🟦 **TypeScript** — `Promise.all`: +```typescript +const [user, posts] = await Promise.all([ + fetchUser(42), + fetchPosts(42), +]); +``` + +🦀 **Rust** — `tokio::join!`: +```rust +let (user, posts) = tokio::join!( + fetch_user(42), + fetch_posts(42), +); +``` + +## Spawning Tasks + +🟦 **TypeScript** — fire and forget: +```typescript +// In TypeScript, just calling an async function without await starts it +fetchUser(42); // runs in the background (but you lose error handling) +``` + +🦀 **Rust** — explicit spawn: +```rust +let handle = tokio::spawn(async { + fetch_user(42).await +}); + +// Later: +let user = handle.await.unwrap(); +``` + +## `select!` — Racing Futures + +Like `Promise.race` but more powerful: + +```rust +use tokio::select; + +select! { + user = fetch_user(42) => println!("got user: {user:?}"), + _ = tokio::time::sleep(Duration::from_secs(5)) => println!("timeout!"), +} +``` + +## Streams — Async Iterators + +TypeScript has `AsyncIterable`. Rust's equivalent is `Stream` (from `tokio-stream` or +`futures` crate): + +🟦 **TypeScript** +```typescript +for await (const chunk of response.body) { + process(chunk); +} +``` + +🦀 **Rust** +```rust +use tokio_stream::StreamExt; + +let mut stream = tokio_stream::iter(vec![1, 2, 3]); +while let Some(value) = stream.next().await { + println!("{value}"); +} +``` + +## Common Async Patterns + +### Timeout + +```rust +use tokio::time::{timeout, Duration}; + +match timeout(Duration::from_secs(5), fetch_user(42)).await { + Ok(Ok(user)) => println!("got user: {user:?}"), + Ok(Err(e)) => println!("request failed: {e}"), + Err(_) => println!("timed out"), +} +``` + +### Retry with Backoff + +```rust +let mut delay = Duration::from_millis(100); +for attempt in 1..=3 { + match fetch_user(42).await { + Ok(user) => return Ok(user), + Err(e) if attempt < 3 => { + eprintln!("attempt {attempt} failed: {e}, retrying..."); + tokio::time::sleep(delay).await; + delay *= 2; + } + Err(e) => return Err(e), + } +} +``` + +🏋️ **Exercise**: Write an async function that fetches 3 URLs concurrently with `tokio::join!` +and returns the first successful result, or all errors if all fail. diff --git a/typescript-book/src/ch13-concurrency.md b/typescript-book/src/ch13-concurrency.md new file mode 100644 index 0000000..67ac00a --- /dev/null +++ b/typescript-book/src/ch13-concurrency.md @@ -0,0 +1,169 @@ +# Concurrency + +## The Concurrency Landscape + +| TypeScript | Rust | Model | +|-----------|------|-------| +| Event loop + callbacks | `tokio` / `async-std` | Cooperative async | +| `Promise` / `async`/`await` | `Future` / `async`/`await` | Async I/O | +| `Worker` threads | `std::thread` | OS threads | +| `SharedArrayBuffer` | `Arc>` | Shared state | +| `postMessage` | `mpsc::channel` | Message passing | + +💡 **Key insight**: TypeScript is single-threaded by design — concurrency means interleaving +I/O, not parallel computation. Rust gives you both: async for I/O concurrency and threads for +CPU parallelism, with compile-time safety guarantees for both. + +## Threads + +```rust +use std::thread; + +let handle = thread::spawn(|| { + println!("hello from a thread!"); + 42 +}); + +let result = handle.join().unwrap(); // waits for thread, gets return value +assert_eq!(result, 42); +``` + +### Moving Data into Threads + +```rust +let data = vec![1, 2, 3]; + +let handle = thread::spawn(move || { + // `move` transfers ownership of `data` into this thread + println!("sum: {}", data.iter().sum::()); +}); + +// data is no longer accessible here — it was moved +handle.join().unwrap(); +``` + +## Message Passing — Channels + +🟦 **TypeScript** (Worker threads): +```typescript +worker.postMessage({ type: "process", data: items }); +worker.on("message", (result) => { /* ... */ }); +``` + +🦀 **Rust** (channels): +```rust +use std::sync::mpsc; + +let (tx, rx) = mpsc::channel(); + +thread::spawn(move || { + tx.send("hello from thread").unwrap(); + tx.send("another message").unwrap(); +}); + +// Receive messages +for msg in rx { + println!("received: {msg}"); +} +``` + +`mpsc` stands for *multiple producer, single consumer*. Clone the sender for multiple +producers: + +```rust +let (tx, rx) = mpsc::channel(); +let tx2 = tx.clone(); + +thread::spawn(move || tx.send("from thread 1").unwrap()); +thread::spawn(move || tx2.send("from thread 2").unwrap()); + +for msg in rx { + println!("{msg}"); +} +``` + +## Shared State — `Mutex` and `Arc` + +### `Mutex` — Mutual exclusion + +```rust +use std::sync::Mutex; + +let counter = Mutex::new(0); + +{ + let mut num = counter.lock().unwrap(); // acquire lock + *num += 1; +} // lock is released when `num` goes out of scope +``` + +### `Arc` — Atomic Reference Counting + +To share data across threads, wrap it in `Arc` (atomic `Rc`): + +```rust +use std::sync::{Arc, Mutex}; +use std::thread; + +let counter = Arc::new(Mutex::new(0)); +let mut handles = vec![]; + +for _ in 0..10 { + let counter = Arc::clone(&counter); + let handle = thread::spawn(move || { + let mut num = counter.lock().unwrap(); + *num += 1; + }); + handles.push(handle); +} + +for handle in handles { + handle.join().unwrap(); +} + +println!("final count: {}", *counter.lock().unwrap()); // 10 +``` + +## `Send` and `Sync` — Compile-Time Thread Safety + +Rust prevents data races at compile time using two marker traits: + +- **`Send`**: A type can be transferred to another thread. +- **`Sync`**: A type can be referenced from multiple threads. + +Most types are both `Send` and `Sync`. Notable exceptions: +- `Rc` is neither (use `Arc` instead). +- `Cell` / `RefCell` are `Send` but not `Sync`. +- Raw pointers are neither. + +If you try to send a non-`Send` type to another thread, you get a compile error — not a +runtime data race. + +```rust +use std::rc::Rc; + +let data = Rc::new(42); +thread::spawn(move || { + println!("{data}"); // ❌ compile error: Rc cannot be sent between threads +}); +``` + +## `RwLock` — Multiple Readers, One Writer + +When reads are far more common than writes: + +```rust +use std::sync::RwLock; + +let config = RwLock::new(Config::default()); + +// Many readers can hold the lock simultaneously +let cfg = config.read().unwrap(); + +// Only one writer, and it blocks readers +let mut cfg = config.write().unwrap(); +cfg.debug = true; +``` + +🏋️ **Exercise**: Create a program that spawns 4 threads, each generating 1000 random numbers. +Use a channel to send the numbers to the main thread, which collects them into a sorted `Vec`. diff --git a/typescript-book/src/ch14-1-webassembly.md b/typescript-book/src/ch14-1-webassembly.md new file mode 100644 index 0000000..03674ac --- /dev/null +++ b/typescript-book/src/ch14-1-webassembly.md @@ -0,0 +1,139 @@ +# WebAssembly and wasm-bindgen + +## Why This Matters for TypeScript Developers + +WebAssembly (Wasm) is where Rust and TypeScript intersect most directly. You can compile Rust +to Wasm and call it from your TypeScript/JavaScript application — getting native-speed +performance for compute-heavy tasks while keeping your UI in TypeScript. + +## Use Cases + +- **Image/video processing** — filters, resizing, encoding. +- **Cryptography** — hashing, encryption in the browser. +- **Data transformation** — parsing large CSV/JSON files. +- **Games** — physics engines, pathfinding. +- **Compression** — gzip, brotli, zstd. + +## Setup with wasm-pack + +```bash +cargo install wasm-pack +cargo new --lib my-wasm-lib +``` + +```toml +# Cargo.toml +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2" +``` + +## Hello Wasm + +```rust +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn greet(name: &str) -> String { + format!("Hello, {name}!") +} + +#[wasm_bindgen] +pub fn fibonacci(n: u32) -> u64 { + match n { + 0 => 0, + 1 => 1, + _ => { + let (mut a, mut b) = (0u64, 1u64); + for _ in 2..=n { + let temp = a + b; + a = b; + b = temp; + } + b + } + } +} +``` + +Build: +```bash +wasm-pack build --target web +``` + +## Calling from TypeScript + +```typescript +import init, { greet, fibonacci } from "./pkg/my_wasm_lib.js"; + +async function main() { + await init(); + console.log(greet("TypeScript")); // "Hello, TypeScript!" + console.log(fibonacci(50)); // 12586269025 +} + +main(); +``` + +## Passing Complex Types + +### Structs + +```rust +#[wasm_bindgen] +pub struct Point { + pub x: f64, + pub y: f64, +} + +#[wasm_bindgen] +impl Point { + #[wasm_bindgen(constructor)] + pub fn new(x: f64, y: f64) -> Self { + Point { x, y } + } + + pub fn distance(&self, other: &Point) -> f64 { + ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt() + } +} +``` + +```typescript +const p1 = new Point(0, 0); +const p2 = new Point(3, 4); +console.log(p1.distance(p2)); // 5.0 +``` + +### Working with `serde` for JSON + +```rust +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub width: u32, + pub height: u32, + pub title: String, +} + +#[wasm_bindgen] +pub fn process_config(val: JsValue) -> Result { + let config: Config = serde_wasm_bindgen::from_value(val)?; + let result = format!("{}x{}: {}", config.width, config.height, config.title); + Ok(JsValue::from_str(&result)) +} +``` + +## Performance Tips + +1. **Minimize boundary crossings** — each JS↔Wasm call has overhead. Batch work. +2. **Use typed arrays** — pass `&[u8]`, `&[f32]` etc. for bulk data instead of individual values. +3. **Avoid strings for hot paths** — string conversion is expensive at the boundary. +4. **Use `web-sys` and `js-sys`** crates to call browser APIs directly from Rust. + +🏋️ **Exercise**: Create a Wasm module with a function that takes a `Vec` of numbers and +returns statistical measures (mean, median, standard deviation). Call it from a TypeScript file. diff --git a/typescript-book/src/ch14-unsafe-rust-and-ffi.md b/typescript-book/src/ch14-unsafe-rust-and-ffi.md new file mode 100644 index 0000000..2e649c9 --- /dev/null +++ b/typescript-book/src/ch14-unsafe-rust-and-ffi.md @@ -0,0 +1,113 @@ +# Unsafe Rust and FFI + +## What Is Unsafe? + +Rust's safety guarantees come from the borrow checker, type system, and lifetime analysis. The +`unsafe` keyword lets you opt out of specific checks when you need to do things the compiler +can't verify. Think of it as TypeScript's `as any` — but scoped, auditable, and necessary only +in rare cases. + +## What Unsafe Allows + +Inside an `unsafe` block, you can: +1. Dereference raw pointers. +2. Call `unsafe` functions. +3. Access mutable static variables. +4. Implement `unsafe` traits. +5. Access fields of `union` types. + +Everything else (borrow checking, type checking, etc.) is still enforced. + +```rust +let mut x = 42; +let ptr = &mut x as *mut i32; // raw pointer — creating is safe + +unsafe { + *ptr = 100; // dereferencing is unsafe +} +``` + +## When You'll Actually Need Unsafe + +As a TypeScript developer coming to Rust, you'll rarely write `unsafe` code yourself. You'll +encounter it in: + +1. **FFI** — calling C libraries or being called from C/Wasm. +2. **Performance-critical hot paths** — avoiding bounds checks in proven-safe contexts. +3. **Low-level data structures** — implementing things like custom allocators. + +## FFI — Calling C from Rust + +```rust +extern "C" { + fn abs(input: i32) -> i32; + fn strlen(s: *const u8) -> usize; +} + +fn main() { + unsafe { + println!("abs(-5) = {}", abs(-5)); + } +} +``` + +## FFI — Calling Rust from C / Node.js + +You can create a shared library and call it from Node.js via `node-ffi` or N-API: + +```rust +// lib.rs +#[no_mangle] +pub extern "C" fn add(a: i32, b: i32) -> i32 { + a + b +} +``` + +```toml +# Cargo.toml +[lib] +crate-type = ["cdylib"] +``` + +## Safe Abstractions over Unsafe Code + +The Rust idiom is to write a small `unsafe` core and wrap it in a safe API: + +```rust +pub struct SafeBuffer { + data: *mut u8, + len: usize, +} + +impl SafeBuffer { + pub fn new(size: usize) -> Self { + let data = unsafe { + std::alloc::alloc(std::alloc::Layout::array::(size).unwrap()) + }; + SafeBuffer { data, len: size } + } + + pub fn get(&self, index: usize) -> Option { + if index < self.len { + Some(unsafe { *self.data.add(index) }) + } else { + None + } + } +} + +impl Drop for SafeBuffer { + fn drop(&mut self) { + unsafe { + std::alloc::dealloc( + self.data, + std::alloc::Layout::array::(self.len).unwrap(), + ); + } + } +} +``` + +Users of `SafeBuffer` never need `unsafe` — the API ensures correctness. + +🏋️ **Exercise**: Write a safe wrapper around `libc::getenv` that returns `Option`. diff --git a/typescript-book/src/ch15-migration-patterns.md b/typescript-book/src/ch15-migration-patterns.md new file mode 100644 index 0000000..b0ed22c --- /dev/null +++ b/typescript-book/src/ch15-migration-patterns.md @@ -0,0 +1,188 @@ +# Migration Patterns + +## Strategies for Introducing Rust into a TypeScript Codebase + +You don't need to rewrite everything. Here are battle-tested strategies for incremental +adoption. + +## Strategy 1: Wasm Module for Hot Paths + +Keep your TypeScript application and replace performance-critical functions with Rust compiled +to WebAssembly. This is the lowest-risk approach. + +**Example**: A data processing pipeline. + +``` +┌─────────────────────────────────────┐ +│ TypeScript Application │ +│ │ +│ UI ──► Parser ──► Transformer ──► │ +│ (Rust/ (Rust/Wasm) │ +│ Wasm) │ +└─────────────────────────────────────┘ +``` + +## Strategy 2: Rust CLI Tool + +Replace Node.js scripts and build tools with Rust CLIs. These are standalone binaries with +instant startup and no runtime dependency. + +```bash +# Before: Node.js script (requires node, npm install) +node scripts/process-data.js --input data.csv + +# After: Rust binary (single file, instant start) +./process-data --input data.csv +``` + +## Strategy 3: Rust Microservice + +Run Rust as a separate HTTP service alongside your TypeScript backend: + +``` +┌──────────────┐ HTTP/gRPC ┌──────────────┐ +│ TypeScript │ ◄──────────────► │ Rust │ +│ API Gateway │ │ Compute Svc │ +└──────────────┘ └──────────────┘ +``` + +## Strategy 4: N-API Native Module + +For Node.js applications, compile Rust as a native addon using `napi-rs`: + +```rust +use napi_derive::napi; + +#[napi] +pub fn sum(a: i32, b: i32) -> i32 { + a + b +} +``` + +```typescript +import { sum } from "./index.node"; +console.log(sum(2, 3)); // 5 +``` + +## TypeScript → Rust Translation Patterns + +### Classes → Structs + Impl + Traits + +🟦 **TypeScript** +```typescript +abstract class Animal { + constructor(public name: string) {} + abstract speak(): string; + greet(): string { return `I'm ${this.name}`; } +} + +class Dog extends Animal { + speak() { return "Woof!"; } +} +``` + +🦀 **Rust** +```rust +trait Animal { + fn name(&self) -> &str; + fn speak(&self) -> String; + fn greet(&self) -> String { + format!("I'm {}", self.name()) + } +} + +struct Dog { name: String } + +impl Animal for Dog { + fn name(&self) -> &str { &self.name } + fn speak(&self) -> String { "Woof!".to_string() } +} +``` + +### Inheritance → Composition + Traits + +Rust has no inheritance. Use composition: + +```rust +struct Logger { prefix: String } +struct Database { connection: String } + +struct App { + logger: Logger, + db: Database, +} +``` + +### Optional Fields → `Option` + +```typescript +interface Config { + host: string; + port?: number; + debug?: boolean; +} +``` + +```rust +struct Config { + host: String, + port: Option, + debug: Option, +} +``` + +### Builder Pattern (replaces optional constructor arguments) + +```rust +struct ServerConfig { + host: String, + port: u16, + workers: usize, +} + +struct ServerConfigBuilder { + host: String, + port: u16, + workers: usize, +} + +impl ServerConfigBuilder { + fn new(host: impl Into) -> Self { + Self { host: host.into(), port: 8080, workers: 4 } + } + + fn port(mut self, port: u16) -> Self { self.port = port; self } + fn workers(mut self, n: usize) -> Self { self.workers = n; self } + + fn build(self) -> ServerConfig { + ServerConfig { + host: self.host, + port: self.port, + workers: self.workers, + } + } +} + +let config = ServerConfigBuilder::new("localhost") + .port(3000) + .workers(8) + .build(); +``` + +### Callbacks → Closures or Channels + +```typescript +function processAsync(data: string, callback: (result: string) => void) { + setTimeout(() => callback(data.toUpperCase()), 100); +} +``` + +```rust +fn process_async(data: &str, callback: impl FnOnce(String)) { + let result = data.to_uppercase(); + callback(result); +} +``` + +🏋️ **Exercise**: Take a TypeScript module from one of your projects and translate it to Rust. +Start with a simple utility module with pure functions. diff --git a/typescript-book/src/ch16-1-avoiding-excessive-clone.md b/typescript-book/src/ch16-1-avoiding-excessive-clone.md new file mode 100644 index 0000000..9b34a5d --- /dev/null +++ b/typescript-book/src/ch16-1-avoiding-excessive-clone.md @@ -0,0 +1,91 @@ +# Avoiding Excessive clone() + +## The Problem + +When learning Rust, it's tempting to `.clone()` everything to silence the borrow checker. This +works but defeats Rust's zero-cost philosophy — every clone is a heap allocation. + +## When clone() Is Fine + +- **Small, infrequently-cloned data** — a configuration string cloned once at startup. +- **Prototyping** — get it working first, optimize later. +- **Shared ownership semantics** — when you genuinely need independent copies. +- **Arc::clone()** — this only increments a reference count, not a deep copy. + +## When to Avoid clone() + +### Pattern 1: Borrow Instead of Clone + +```rust +// ❌ Clones the entire string just to read it +fn print_name(user: &User) { + let name = user.name.clone(); + println!("{name}"); +} + +// ✅ Borrow it +fn print_name(user: &User) { + println!("{}", user.name); +} +``` + +### Pattern 2: Take Ownership When You Need It + +```rust +// ❌ Clone then discard original +fn process(items: &Vec) { + let owned = items.clone(); + consume(owned); +} + +// ✅ Take ownership directly +fn process(items: Vec) { + consume(items); +} +``` + +### Pattern 3: Use Cow for Maybe-Owned Data + +`Cow` (Clone-on-Write) borrows when possible and only clones when mutation is needed: + +```rust +use std::borrow::Cow; + +fn normalize(input: &str) -> Cow<'_, str> { + if input.contains(' ') { + Cow::Owned(input.replace(' ', "-")) // allocates only when needed + } else { + Cow::Borrowed(input) // zero-cost borrow + } +} +``` + +### Pattern 4: Return References from Methods + +```rust +struct Database { + records: Vec, +} + +impl Database { + // ❌ Clones the record + fn find(&self, id: u64) -> Option { + self.records.iter().find(|r| r.id == id).cloned() + } + + // ✅ Returns a reference + fn find(&self, id: u64) -> Option<&Record> { + self.records.iter().find(|r| r.id == id) + } +} +``` + +## Profiling Clone Usage + +Use `cargo clippy` to find unnecessary clones: +```bash +cargo clippy -- -W clippy::redundant_clone +``` + +🏋️ **Exercise**: Review a Rust program you've written and identify every `.clone()` call. +For each, determine if it can be replaced with a borrow, ownership transfer, or `Cow`. diff --git a/typescript-book/src/ch16-2-logging-and-tracing-ecosystem.md b/typescript-book/src/ch16-2-logging-and-tracing-ecosystem.md new file mode 100644 index 0000000..5c2f9fc --- /dev/null +++ b/typescript-book/src/ch16-2-logging-and-tracing-ecosystem.md @@ -0,0 +1,87 @@ +# Logging and Tracing Ecosystem + +## From console.log to Structured Logging + +| TypeScript | Rust | Purpose | +|-----------|------|---------| +| `console.log` | `println!` | Quick debugging | +| `winston` / `pino` | `tracing` + `tracing-subscriber` | Structured logging | +| `morgan` (HTTP) | `tower-http::trace` | Request logging | + +## The `tracing` Crate + +`tracing` is the de facto standard for logging and instrumentation in Rust: + +```toml +[dependencies] +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +``` + +```rust +use tracing::{info, warn, error, debug, trace, instrument}; + +#[instrument] +fn process_order(order_id: u64, customer: &str) { + info!(order_id, customer, "processing order"); + + if order_id == 0 { + warn!("suspicious order ID"); + } + + debug!(order_id, "order validated"); +} + +fn main() { + tracing_subscriber::fmt() + .with_env_filter("my_app=debug") + .init(); + + process_order(42, "Alice"); +} +``` + +Output: +``` +2025-03-24T10:30:00.000Z DEBUG my_app: order validated order_id=42 +2025-03-24T10:30:00.000Z INFO my_app: processing order order_id=42 customer="Alice" +``` + +## Log Levels + +Same concept as in TypeScript logging libraries: + +| Level | Use | +|-------|-----| +| `error!` | Something broke | +| `warn!` | Something is wrong but recoverable | +| `info!` | Normal operations | +| `debug!` | Detailed diagnostic information | +| `trace!` | Very verbose, per-iteration data | + +## Structured Fields + +```rust +info!( + user_id = 42, + action = "login", + ip = "192.168.1.1", + "user logged in" +); +``` + +## The `#[instrument]` Attribute + +Automatically creates a span with function arguments: + +```rust +#[instrument(skip(password))] // skip sensitive fields +async fn authenticate(username: &str, password: &str) -> Result { + // All logs inside this function are automatically tagged with username + info!("authenticating"); + // ... +} +``` + +🏋️ **Exercise**: Add `tracing` to one of your capstone project's modules. Log at appropriate +levels and use `#[instrument]` on async functions. diff --git a/typescript-book/src/ch16-best-practices.md b/typescript-book/src/ch16-best-practices.md new file mode 100644 index 0000000..e773f2f --- /dev/null +++ b/typescript-book/src/ch16-best-practices.md @@ -0,0 +1,99 @@ +# Best Practices + +## Idiomatic Rust for TypeScript Developers + +### Prefer `&str` over `String` in Function Parameters + +```rust +// ❌ Requires callers to allocate a String +fn greet(name: String) -> String { format!("Hello, {name}!") } + +// ✅ Accepts both &str and &String +fn greet(name: &str) -> String { format!("Hello, {name}!") } +``` + +### Use `impl Into` for Flexible APIs + +```rust +fn connect(host: impl Into, port: u16) -> Connection { + let host = host.into(); + // ... +} + +connect("localhost", 8080); // &str +connect(String::from("localhost"), 8080); // String +``` + +### Prefer Iterators over Index Loops + +```rust +// ❌ C-style loop +for i in 0..items.len() { + process(&items[i]); +} + +// ✅ Iterator +for item in &items { + process(item); +} +``` + +### Use `clippy` Religiously + +`cargo clippy` catches hundreds of common mistakes and anti-patterns. Run it on every commit, +like you'd run ESLint. Add it to CI: + +```yaml +- run: cargo clippy -- -D warnings +``` + +### Derive What You Can + +```rust +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +struct Config { /* ... */ } +``` + +Derive `Debug` on almost everything. Add `Clone`, `PartialEq` when needed. This is free code +that saves time. + +### Use `todo!()` for Incremental Development + +```rust +fn complex_algorithm(data: &[f64]) -> Vec { + todo!("implement after the basic structure works") +} +``` + +`todo!()` compiles but panics at runtime — perfect for sketching out a program's structure +before filling in the details. Like writing `throw new Error("TODO")` in TypeScript. + +### Prefer `expect()` over `unwrap()` + +```rust +// ❌ Panics with an unhelpful message +let port = env::var("PORT").unwrap(); + +// ✅ Panics with context +let port = env::var("PORT").expect("PORT environment variable must be set"); +``` + +### Use Type-State Patterns + +Instead of runtime checks for invalid state transitions, encode valid states in the type +system (see Chapter 10's phantom types section). + +### Error Messages That Help + +```rust +// ❌ Generic error +return Err("invalid input".into()); + +// ✅ Specific, actionable error +return Err(format!( + "expected port between 1 and 65535, got {port}" +).into()); +``` + +🏋️ **Exercise**: Take a Rust program you've written during this book and run `cargo clippy` +on it. Fix every warning and note the patterns clippy teaches you. diff --git a/typescript-book/src/ch17-ts-rust-semantic-deep-dives.md b/typescript-book/src/ch17-ts-rust-semantic-deep-dives.md new file mode 100644 index 0000000..0f364af --- /dev/null +++ b/typescript-book/src/ch17-ts-rust-semantic-deep-dives.md @@ -0,0 +1,157 @@ +# TypeScript → Rust Semantic Deep Dives + +## Concepts That Look Similar but Behave Differently + +This chapter catalogs the subtle semantic differences between TypeScript and Rust that trip +up experienced developers. + +## 1. Equality + +🟦 **TypeScript**: `===` compares by reference for objects, by value for primitives. +🦀 **Rust**: `==` always uses the `PartialEq` trait — defaults to value comparison for derived +types. + +```rust +let a = String::from("hello"); +let b = String::from("hello"); +assert_eq!(a, b); // true — compares contents, not pointer identity + +// For reference identity, compare pointers: +let x = &a; +let y = &a; +assert!(std::ptr::eq(x, y)); // true — same address +``` + +## 2. Truthiness + +🟦 **TypeScript**: `""`, `0`, `null`, `undefined`, `NaN`, `false` are all falsy. +🦀 **Rust**: Only `bool` can be used in conditions. No implicit conversions. + +```rust +let s = String::new(); +// if s { } // ❌ compile error +if !s.is_empty() { } // ✅ +``` + +## 3. Destructuring with Ownership + +🟦 **TypeScript**: Destructuring always copies the reference. +🦀 **Rust**: Destructuring can *move* fields out of a struct. + +```rust +struct Pair { name: String, value: i32 } + +let pair = Pair { name: "x".into(), value: 42 }; +let Pair { name, value } = pair; +// `pair` is now partially moved — name was moved, value was copied +// println!("{}", pair.name); // ❌ moved +// println!("{}", pair.value); // ❌ partial move prevents this too +``` + +## 4. Method Resolution + +🟦 **TypeScript**: Method lookup walks the prototype chain at runtime. +🦀 **Rust**: Method lookup uses auto-ref and auto-deref at compile time. + +```rust +let s = String::from("hello"); +s.len(); // compiler auto-borrows: (&s).len() +(&s).len(); // explicit borrow — same thing +(&&s).len(); // auto-deref through multiple references +``` + +## 5. Closures and Ownership + +🟦 **TypeScript**: Closures always capture by reference. Variables are shared. +🦀 **Rust**: Closures capture by the least restrictive mode needed (borrow → mut borrow → move). + +```rust +let mut name = String::from("Alice"); + +// This closure borrows `name` immutably +let greet = || println!("Hello, {name}"); + +// This closure borrows `name` mutably +let mut update = || name.push_str(" Smith"); + +// This closure moves `name` into itself +let consume = move || println!("Consumed: {name}"); +``` + +## 6. Iteration and Ownership + +🟦 **TypeScript**: `for...of` never consumes the iterable. +🦀 **Rust**: `for x in collection` consumes it by default. + +```rust +let v = vec![1, 2, 3]; + +for x in &v { } // borrows — v is still usable +for x in &mut v { } // mutably borrows +for x in v { } // MOVES — v is consumed, no longer usable +``` + +## 7. String Concatenation + +🟦 **TypeScript**: `"a" + "b"` just works. +🦀 **Rust**: String operations are explicit about ownership. + +```rust +let a = String::from("hello"); +let b = String::from(" world"); + +// These all work but differently: +let c = a + &b; // a is MOVED, b is borrowed. a is now invalid. +let c = format!("{a}{b}"); // neither moved — uses references +let c = [a.as_str(), b.as_str()].concat(); // borrows both +``` + +## 8. Default Values + +🟦 **TypeScript**: `const x = opts.value ?? "default";` +🦀 **Rust**: `let x = opts.value.unwrap_or("default".to_string());` + +```rust +// More options: +let x = opts.value.unwrap_or_default(); // uses Default trait +let x = opts.value.unwrap_or_else(|| expensive()); // lazy default +let x = opts.value.map_or("fallback", |v| v); // map + default +``` + +## 9. Spread / Rest + +🟦 **TypeScript**: `const [first, ...rest] = arr;` +🦀 **Rust**: Pattern matching with slices: + +```rust +let v = vec![1, 2, 3, 4, 5]; +match v.as_slice() { + [first, rest @ ..] => println!("first: {first}, rest: {rest:?}"), + [] => println!("empty"), +} +``` + +## 10. Typeof / Type Checking at Runtime + +🟦 **TypeScript**: `typeof x === "string"`, `x instanceof MyClass` +🦀 **Rust**: No runtime type information. Use enums or trait objects. + +```rust +// Instead of runtime type checks, use enums: +enum Value { + Str(String), + Num(f64), + Bool(bool), +} + +fn process(v: &Value) { + match v { + Value::Str(s) => println!("string: {s}"), + Value::Num(n) => println!("number: {n}"), + Value::Bool(b) => println!("bool: {b}"), + } +} +``` + +🏋️ **Exercise**: For each of the 10 deep dives above, write a small Rust program that +demonstrates the behavior. Try to predict the output before running it. diff --git a/typescript-book/src/ch18-capstone-project.md b/typescript-book/src/ch18-capstone-project.md new file mode 100644 index 0000000..e4957c5 --- /dev/null +++ b/typescript-book/src/ch18-capstone-project.md @@ -0,0 +1,317 @@ +# Capstone Project: REST API Server + +## Overview + +In this capstone, you'll build a REST API server in Rust — the same kind of thing you'd build +with Express, Fastify, or Hono in TypeScript. This ties together everything from the book: +structs, traits, error handling, async, modules, and testing. + +## The Stack + +| TypeScript | Rust | Role | +|-----------|------|------| +| Express / Fastify | `axum` | HTTP framework | +| Prisma / Drizzle | `sqlx` | Database access | +| Zod | `serde` + `validator` | Validation & serialization | +| dotenv | `dotenvy` | Environment config | +| Jest / Vitest | `cargo test` + `reqwest` | Testing | + +## Project Structure + +``` +todo-api/ +├── Cargo.toml +├── .env +├── migrations/ +│ └── 001_create_todos.sql +├── src/ +│ ├── main.rs # entry point, server startup +│ ├── config.rs # configuration from env +│ ├── routes/ +│ │ ├── mod.rs +│ │ └── todos.rs # route handlers +│ ├── models/ +│ │ ├── mod.rs +│ │ └── todo.rs # data structures +│ ├── db.rs # database connection pool +│ └── error.rs # error types +└── tests/ + └── api_tests.rs # integration tests +``` + +## Step 1: Dependencies + +```toml +[dependencies] +axum = "0.7" +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } +dotenvy = "0.15" +tracing = "0.1" +tracing-subscriber = "0.3" +tower-http = { version = "0.5", features = ["cors", "trace"] } +thiserror = "2" +uuid = { version = "1", features = ["v4", "serde"] } + +[dev-dependencies] +reqwest = { version = "0.12", features = ["json"] } +``` + +## Step 2: Models + +```rust +// src/models/todo.rs +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] +pub struct Todo { + pub id: String, + pub title: String, + pub completed: bool, +} + +#[derive(Debug, Deserialize)] +pub struct CreateTodo { + pub title: String, +} + +#[derive(Debug, Deserialize)] +pub struct UpdateTodo { + pub title: Option, + pub completed: Option, +} + +impl Todo { + pub fn new(title: String) -> Self { + Self { + id: Uuid::new_v4().to_string(), + title, + completed: false, + } + } +} +``` + +## Step 3: Error Handling + +```rust +// src/error.rs +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use serde_json::json; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum AppError { + #[error("not found: {0}")] + NotFound(String), + + #[error("bad request: {0}")] + BadRequest(String), + + #[error("internal error: {0}")] + Internal(String), + + #[error(transparent)] + Database(#[from] sqlx::Error), +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, message) = match &self { + AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()), + AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()), + AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()), + AppError::Database(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), + }; + + (status, Json(json!({ "error": message }))).into_response() + } +} +``` + +## Step 4: Route Handlers + +```rust +// src/routes/todos.rs +use axum::extract::{Path, State}; +use axum::Json; +use sqlx::SqlitePool; + +use crate::error::AppError; +use crate::models::todo::{CreateTodo, Todo, UpdateTodo}; + +pub async fn list_todos( + State(pool): State, +) -> Result>, AppError> { + let todos = sqlx::query_as::<_, Todo>("SELECT id, title, completed FROM todos") + .fetch_all(&pool) + .await?; + Ok(Json(todos)) +} + +pub async fn create_todo( + State(pool): State, + Json(input): Json, +) -> Result, AppError> { + if input.title.trim().is_empty() { + return Err(AppError::BadRequest("title cannot be empty".into())); + } + + let todo = Todo::new(input.title); + sqlx::query("INSERT INTO todos (id, title, completed) VALUES (?, ?, ?)") + .bind(&todo.id) + .bind(&todo.title) + .bind(todo.completed) + .execute(&pool) + .await?; + + Ok(Json(todo)) +} + +pub async fn get_todo( + State(pool): State, + Path(id): Path, +) -> Result, AppError> { + let todo = sqlx::query_as::<_, Todo>( + "SELECT id, title, completed FROM todos WHERE id = ?" + ) + .bind(&id) + .fetch_optional(&pool) + .await? + .ok_or_else(|| AppError::NotFound(format!("todo {id} not found")))?; + + Ok(Json(todo)) +} + +pub async fn update_todo( + State(pool): State, + Path(id): Path, + Json(input): Json, +) -> Result, AppError> { + let existing = sqlx::query_as::<_, Todo>( + "SELECT id, title, completed FROM todos WHERE id = ?" + ) + .bind(&id) + .fetch_optional(&pool) + .await? + .ok_or_else(|| AppError::NotFound(format!("todo {id} not found")))?; + + let title = input.title.unwrap_or(existing.title); + let completed = input.completed.unwrap_or(existing.completed); + + sqlx::query("UPDATE todos SET title = ?, completed = ? WHERE id = ?") + .bind(&title) + .bind(completed) + .bind(&id) + .execute(&pool) + .await?; + + Ok(Json(Todo { id, title, completed })) +} + +pub async fn delete_todo( + State(pool): State, + Path(id): Path, +) -> Result { + let result = sqlx::query("DELETE FROM todos WHERE id = ?") + .bind(&id) + .execute(&pool) + .await?; + + if result.rows_affected() == 0 { + return Err(AppError::NotFound(format!("todo {id} not found"))); + } + + Ok(StatusCode::NO_CONTENT) +} +``` + +## Step 5: Main Entry Point + +```rust +// src/main.rs +mod config; +mod db; +mod error; +mod models; +mod routes; + +use axum::{routing::{get, post, put, delete}, Router}; +use tower_http::trace::TraceLayer; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + + let pool = db::create_pool().await?; + + let app = Router::new() + .route("/todos", get(routes::todos::list_todos)) + .route("/todos", post(routes::todos::create_todo)) + .route("/todos/:id", get(routes::todos::get_todo)) + .route("/todos/:id", put(routes::todos::update_todo)) + .route("/todos/:id", delete(routes::todos::delete_todo)) + .layer(TraceLayer::new_for_http()) + .with_state(pool); + + let addr = "0.0.0.0:3000"; + tracing::info!("listening on {addr}"); + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app).await?; + Ok(()) +} +``` + +## Step 6: Integration Tests + +```rust +// tests/api_tests.rs +use reqwest::Client; +use serde_json::json; + +#[tokio::test] +async fn test_create_and_get_todo() { + let client = Client::new(); + let base = "http://localhost:3000"; + + // Create + let res = client.post(format!("{base}/todos")) + .json(&json!({ "title": "Learn Rust" })) + .send() + .await + .unwrap(); + + assert_eq!(res.status(), 200); + let todo: serde_json::Value = res.json().await.unwrap(); + let id = todo["id"].as_str().unwrap(); + + // Get + let res = client.get(format!("{base}/todos/{id}")) + .send() + .await + .unwrap(); + + assert_eq!(res.status(), 200); + let fetched: serde_json::Value = res.json().await.unwrap(); + assert_eq!(fetched["title"], "Learn Rust"); + assert_eq!(fetched["completed"], false); +} +``` + +## What You've Learned + +By completing this capstone, you've used: +- **Structs and enums** for data modeling. +- **Traits** (`FromRow`, `IntoResponse`, `Serialize`, `Deserialize`). +- **Error handling** with `thiserror` and `?`. +- **Async/await** with `tokio` and `axum`. +- **Modules** for project organization. +- **Testing** with `cargo test`. +- **Logging** with `tracing`. + +Congratulations — you're now a Rust developer. 🦀