PONY λ M2 Modula-2

Rust.CodeCompared.To/TypeScript

An interactive executable cheatsheet comparing Rust and TypeScript

Rust 1.95 TypeScript 6.0
Hello World & Tooling
Hello, World
fn main() { println!("Hello, World!"); }
console.log("Hello, World!");
TypeScript needs no fn main entry point — top-level statements run in order. console.log is the everyday equivalent of Rust's println! macro; both add a trailing newline. TypeScript code is transpiled to JavaScript before running — there is no binary output.
Build tools & runtimes
// Cargo (built into Rust): // cargo new myproject — create a new project // cargo build --release — compile to a native binary // cargo run — compile + run // cargo test — run tests // cargo add serde — add a dependency // The output is a native machine-code binary — no runtime needed.
// Node.js ecosystem: // npm init -y — create a new project (package.json) // tsc --init — generate tsconfig.json // tsc — compile .ts → .js (then: node dist/index.js) // npx tsx index.ts — compile + run without a separate build step // npm test — run tests (jest, vitest, etc.) // npm install lodash — add a dependency // TypeScript is always transpiled to JavaScript before running — // there is no native binary output. The Node.js (or Deno/Bun) runtime // executes the resulting JavaScript.
Rust compiles to a standalone native binary that requires no runtime to run. TypeScript always produces JavaScript first — you need Node.js, Deno, or Bun to execute it. tsx (from the npm package of the same name) compiles and runs TypeScript in one step without a separate tsc pass, which is useful for development and scripting. Deno and Bun also run TypeScript natively.
Formatted output
fn main() { let name = "Alice"; let score = 42; let ratio = 0.75; println!("{} scored {} ({:.1}%)", name, score, ratio * 100.0); eprintln!("debug: {}", score); // stderr }
const name = "Alice"; const score = 42; const ratio = 0.75; console.log(`${name} scored ${score} (${(ratio * 100).toFixed(1)}%)`); console.error(`debug: ${score}`); // stderr
TypeScript uses template literals (backtick strings with ${expr}) for interpolation, while Rust uses the println! macro with {} placeholders. TypeScript's template literals are more concise but less structured — there is no :.2f format specifier; use methods like .toFixed(2) instead. console.error writes to stderr, matching Rust's eprintln!.
Variables & Types
let and const — mutability
fn main() { let immutable = 42; // immutable binding by default // immutable = 43; // compile error let mut counter = 0; // opt in to mutation with mut counter += 1; println!("{} {}", immutable, counter); }
const immutable = 42; // cannot be reassigned // immutable = 43; // TypeError (runtime) / TS error (compile time) let counter = 0; // let allows reassignment counter += 1; console.log(immutable, counter);
TypeScript's const prevents reassignment of the binding — not deep immutability. An array or object declared with const can still have its contents mutated; only the variable itself cannot be rebound to a different value. Rust's let is immutable by default and prevents mutation of the value too, not just rebinding. TypeScript's let allows both rebinding and mutation, like Rust's let mut.
Type annotations and inference
fn main() { let count: i32 = 42; // explicit type let ratio: f64 = 3.14; // explicit type let inferred = "hello"; // &str inferred println!("{} {} {}", count, ratio, inferred); }
const count: number = 42; // explicit type annotation const ratio: number = 3.14; const inferred = "hello"; // string inferred by TypeScript console.log(count, ratio, inferred); // TypeScript has one number type; Rust has i8/i16/i32/i64/u8/f32/f64... const big: bigint = 9_007_199_254_740_993n; // BigInt for integers > 2^53 console.log(big);
TypeScript uses the same optional annotation syntax (name: Type) as Rust, but the type system is far simpler for numbers. Where Rust has twelve numeric types (i8i64, u8u64, f32, f64), TypeScript has one: number (a 64-bit IEEE 754 float). Integers above 2^53 require BigInt. TypeScript's inference is powerful and works the same way Rust's does — explicit annotations are optional when the type is clear from context.
Primitive types comparison
fn main() { let integer: i32 = 42; let floating: f64 = 3.14; let boolean: bool = true; let character: char = 'A'; let unit: () = (); // unit type println!("{} {} {} {} {:?}", integer, floating, boolean, character, unit); }
const integer: number = 42; const floating: number = 3.14; // same type as integer! const boolean: boolean = true; const character: string = "A"; // no char type — use single-char string const nothing: undefined = undefined; // closest to () console.log(integer, floating, boolean, character, nothing);
TypeScript collapses Rust's integer/float distinction into a single number type. There is no char type — single characters are one-character strings. TypeScript has undefined (an assigned non-value) and null (an explicit empty value) where Rust has the unit type () for "no meaningful value" and Option::None for optional absence. The void type annotates functions that return undefined (i.e., no useful return value).
any and unknown — escape hatches from the type system
fn main() { // Rust has no 'any' — you must use dyn Trait or generics. // The closest is Box<dyn std::any::Any>: use std::any::Any; let value: Box<dyn Any> = Box::new(42_i32); if let Some(integer) = value.downcast_ref::<i32>() { println!("i32: {}", integer); } }
// any: completely bypasses the type system — dangerous but sometimes necessary. const greeting: any = "hello"; console.log(greeting.toUpperCase()); // TypeScript allows any method on 'any' console.log(greeting.nonExistentMethod); // also allowed — no compile error! // unknown: safe version of any — must narrow the type before using it. function processValue(input: unknown): string { if (typeof input === "string") { return input.toUpperCase(); // safe after narrowing } return String(input); } console.log(processValue("world")); console.log(processValue(42));
any completely disables TypeScript's type checks on that value — it is a total escape hatch from the type system. Rust has no equivalent; every value always has a known type. unknown is the safe alternative: it accepts any value but requires explicit type narrowing before the value can be used in a typed way. Prefer unknown over any whenever you truly don't know the type. The never type is the opposite — a value of type never can never exist (exhausted union, infinite loop, thrown exception).
Type assertions — as and satisfies
fn main() { // Rust uses as for numeric casts — not for type assertions. let count: i64 = 42_i32 as i64; // widening cast let ratio: f64 = count as f64; // integer to float println!("{} {}", count, ratio); // For erasing a concrete type to a trait object: // let item: Box<dyn Trait> = Box::new(concrete_value); }
// 'as' tells TypeScript "trust me, this is type T" — no runtime check. const input: unknown = "hello"; const text = input as string; // assertion: treat input as string console.log(text.toUpperCase()); // 'satisfies' checks the type without changing the inferred type: const palette = { red: [255, 0, 0], green: [0, 255, 0], } satisfies Record<string, [number, number, number]>; // palette.red is still typed as [number, number, number], not number[]: console.log(palette.red[0]);
TypeScript's as is a compile-time assertion that does nothing at runtime — it does not check or convert the value. Rust's as is a numeric cast that does actual conversion at runtime. The satisfies operator (TypeScript 4.9+) validates that a value matches a type while preserving the most specific inferred type for the variable. Never use as to silence a legitimate type error — it removes safety guarantees without fixing the underlying mismatch.
Strings
String types — one vs two
fn main() { let borrowed: &str = "hello"; // compile-time string slice let owned: String = String::from("world"); let combined: String = format!("{} {}", borrowed, owned); println!("{}", combined); println!("len (bytes): {}", combined.len()); }
const borrowed: string = "hello"; // TypeScript has one string type const owned: string = "world"; // no distinction between borrowed and owned const combined: string = borrowed + " " + owned; console.log(combined); console.log("len (chars):", combined.length); // UTF-16 code units, not bytes
Rust has two string types: &str (borrowed, no allocation) and String (owned heap buffer). TypeScript has one: string, which is immutable and garbage-collected. TypeScript's .length counts UTF-16 code units; a character outside the Basic Multilingual Plane (emoji, many CJK characters) counts as two. Rust's .len() returns the byte count; use .chars().count() for Unicode scalar values.
Template literals vs format!
fn main() { let name = "Alice"; let score = 95; let message = format!("{} scored {}", name, score); println!("{}", message); println!("{:.2}", 3.14159); // format with precision }
const name = "Alice"; const score = 95; const message = `${name} scored ${score}`; console.log(message); console.log((3.14159).toFixed(2)); // format with precision
TypeScript's template literals are strings delimited by backticks that can embed any expression with ${...}. Rust's format! macro uses {} placeholders and format specifiers like {:.2}. Template literals are evaluated eagerly at the point of the expression — there is no lazy formatting. For numeric precision, TypeScript uses methods like .toFixed(), .toPrecision(), and .toExponential() rather than in-format specifiers.
Common string methods
fn main() { let text = " Hello, World! "; println!("{}", text.trim()); println!("{}", text.to_lowercase()); println!("{}", text.contains("World")); println!("{}", text.replace("World", "TypeScript")); println!("{}", text.split(", ").count()); }
const text = " Hello, World! "; console.log(text.trim()); console.log(text.toLowerCase()); console.log(text.includes("World")); console.log(text.replace("World", "TypeScript")); console.log(text.split(", ").length);
Rust string methods are called on the value directly and return new values (strings are immutable in both languages). TypeScript string methods are also methods on the string value. Notable name differences: Rust's contains → TypeScript's includes; Rust's to_lowercase → TypeScript's toLowerCase. TypeScript's replace only replaces the first match unless you use a regex with the g flag or replaceAll.
Iterating over characters
fn main() { let greeting = "Hello, 🌍"; for character in greeting.chars() { print!("{} ", character); } println!(); println!("chars: {}", greeting.chars().count()); println!("bytes: {}", greeting.len()); }
const greeting = "Hello, 🌍"; for (const character of greeting) { // for...of iterates Unicode code points process.stdout.write(character + " "); } console.log(); // Spread also iterates by code point: const characters = [...greeting]; console.log("chars:", characters.length); console.log("bytes:", new TextEncoder().encode(greeting).byteLength);
TypeScript's for...of loop over a string iterates by Unicode code point, correctly handling multi-unit characters like emoji. Rust's .chars() also yields Unicode scalar values. However, TypeScript's .length counts UTF-16 code units — a single emoji like 🌍 counts as 2 — so .length does not equal the number of visible characters for strings with supplementary characters. Use [...str].length for accurate character count.
Arrays & Tuples
Arrays and Vec vs TypeScript arrays
fn main() { let fixed: [i32; 3] = [1, 2, 3]; // fixed-size array on stack let mut dynamic: Vec<i32> = vec![1, 2, 3]; // growable heap vector dynamic.push(4); println!("{:?}", fixed); println!("{:?}", dynamic); println!("len: {}", dynamic.len()); }
const fixed: readonly [number, number, number] = [1, 2, 3]; // tuple const dynamic: number[] = [1, 2, 3]; // growable array (TypeScript arrays are always dynamic) dynamic.push(4); console.log(fixed); console.log(dynamic); console.log("len:", dynamic.length);
TypeScript arrays (T[] or Array<T>) are always dynamic — there is no separate fixed-size array type. The closest to Rust's fixed [T; N] is a tuple type ([T, T, T]), which has a fixed length with element types at each position. TypeScript arrays grow with .push() like Rust's Vec::push. Rust's distinction between fixed arrays (stack) and Vec (heap) has no equivalent in TypeScript, where all data is heap-allocated and garbage-collected.
Array iteration and transformation
fn main() { let numbers = vec![1, 2, 3, 4, 5]; let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect(); let evens: Vec<&i32> = numbers.iter().filter(|&&x| x % 2 == 0).collect(); let total: i32 = numbers.iter().sum(); println!("{:?}", doubled); println!("{:?}", evens); println!("total: {}", total); }
const numbers = [1, 2, 3, 4, 5]; const doubled = numbers.map(x => x * 2); const evens = numbers.filter(x => x % 2 === 0); const total = numbers.reduce((accumulator, x) => accumulator + x, 0); console.log(doubled); console.log(evens); console.log("total:", total);
TypeScript arrays have map, filter, and reduce as built-in methods — no iterator adapter chain or .collect() needed. Rust's .iter().map(...).collect() pattern explicitly chains iterator adapters and materializes the result; TypeScript's methods each create a new array immediately. Rust's iterators are lazy (no work done until consumed); TypeScript's array methods are eager. Arrow functions (x => x * 2) are TypeScript's equivalent of Rust's closures.
Tuples — fixed-length heterogeneous sequences
fn main() { let point: (f64, f64) = (3.0, 4.0); let (x, y) = point; // destructure println!("({}, {})", x, y); let named = ("Alice", 95, true); println!("{} scored {} — pass: {}", named.0, named.1, named.2); }
const point: [number, number] = [3.0, 4.0]; const [x, y] = point; // destructure console.log(`(${x}, ${y})`); const named: [string, number, boolean] = ["Alice", 95, true]; console.log(`${named[0]} scored ${named[1]} — pass: ${named[2]}`);
TypeScript uses array syntax for tuples: [string, number, boolean] is a tuple type. Rust's tuple syntax is (string, i32, bool) with parentheses. Both support destructuring assignment. TypeScript tuple types are structural — [string, number] accepts any two-element array with those types in that order. Rust tuples are distinct from arrays: (i32, &str) is a value, while [i32; 2] is a fixed-length homogeneous array.
Spread and rest
fn main() { let first = vec![1, 2, 3]; let second = vec![4, 5, 6]; let combined: Vec<i32> = first.iter().chain(second.iter()).copied().collect(); println!("{:?}", combined); // Destructure with remaining (slice pattern, not tuple pattern): let [head, tail @ ..] = combined.as_slice() else { return }; println!("head: {:?}, tail: {:?}", head, tail); }
const first = [1, 2, 3]; const second = [4, 5, 6]; const combined = [...first, ...second]; // spread into new array console.log(combined); // Rest destructuring: const [head, ...tail] = combined; console.log("head:", head, "tail:", tail);
TypeScript's spread operator (...) creates a shallow copy of an array into a new array. Rust achieves the same with .iter().chain() and .collect() — more explicit but also more composable. Rest destructuring (const [head, ...tail]) mirrors Rust's let [head, tail @ ..]. Spread also works with objects: { ...defaults, ...overrides } merges two objects, equivalent to a manual field-by-field construction in Rust.
Objects & Maps
Objects vs structs
struct Person { name: String, age: u32, } fn main() { let person = Person { name: "Alice".to_string(), age: 30 }; println!("{} is {}", person.name, person.age); }
type Person = { name: string; age: number; }; const person: Person = { name: "Alice", age: 30 }; console.log(`${person.name} is ${person.age}`); // Object type annotation is structural — any object with those fields matches: function greet(entity: { name: string }): string { return `Hello, ${entity.name}!`; } console.log(greet(person));
TypeScript object types (type aliases or interfaces) are structural — any object with the required fields satisfies the type, without needing to declare that it implements anything. Rust structs are nominal — a struct's identity is its name, not its shape; two structs with identical fields are different types. TypeScript objects are plain JavaScript objects with no attached methods unless defined in a class; methods can also be added as function-typed fields.
Map vs HashMap
use std::collections::HashMap; fn main() { let mut scores: HashMap<String, i32> = HashMap::new(); scores.insert("Alice".to_string(), 95); scores.insert("Bob".to_string(), 80); if let Some(&score) = scores.get("Alice") { println!("Alice: {}", score); } for (name, score) in &scores { println!("{}: {}", name, score); } }
const scores = new Map<string, number>([ ["Alice", 95], ["Bob", 80], ]); if (scores.has("Alice")) { console.log("Alice:", scores.get("Alice")); } for (const [name, score] of scores) { console.log(`${name}: ${score}`); }
TypeScript's Map<K, V> is the equivalent of Rust's HashMap<K, V>. Both are key-value stores with explicit lookup. TypeScript also has plain objects (Record<string, V>) for string keys, but Map supports any key type and preserves insertion order. Map.get returns V | undefined (rather than Option<&V>), so the caller must check for undefined before using the value.
Destructuring
struct Point { x: f64, y: f64 } fn main() { let point = Point { x: 3.0, y: 4.0 }; let Point { x, y } = point; // struct destructure println!("({}, {})", x, y); let pair = (10, "hello"); let (number, text) = pair; println!("{} {}", number, text); }
const point = { x: 3.0, y: 4.0 }; const { x, y } = point; // object destructure console.log(`(${x}, ${y})`); const pair: [number, string] = [10, "hello"]; const [number, text] = pair; // array/tuple destructure console.log(number, text); // Rename while destructuring: const { x: horizontal, y: vertical } = point; console.log(horizontal, vertical);
TypeScript destructuring syntax is derived from JavaScript and covers both objects ({ x, y } = obj) and arrays ([first, second] = arr). Rust has destructuring in let bindings and function arguments too, but uses the type's literal syntax (let Point { x, y } = point). TypeScript allows renaming during destructuring with a colon: { x: horizontal } — confusingly this does not add a type annotation; it renames to horizontal.
Optional chaining (?.) vs Option
fn main() { let text: Option<String> = Some("hello".to_string()); // Access inner value — Rust forces you to handle None: let length = text.as_ref().map(|string| string.len()); println!("{:?}", length); let none: Option<String> = None; let safe_length = none.as_ref().map(|string| string.len()).unwrap_or(0); println!("{}", safe_length); }
const text: string | null = "hello"; // Optional chaining returns undefined if any link in the chain is null/undefined: const length = text?.length; console.log(length); // 5 const nothing: string | null = null; const safeLength = nothing?.length ?? 0; // nullish coalescing for default console.log(safeLength); // 0
TypeScript's ?. optional chaining silently short-circuits to undefined if any part of the chain is null or undefined. Rust's equivalent is .as_ref().map(|v| v.field) on an Option. The ?? nullish coalescing operator returns the right side only if the left is null or undefined, matching Rust's .unwrap_or(default). TypeScript has both null and undefined as separate nil-like values; Rust has only None.
Control Flow
if — expression in Rust, statement in TypeScript
fn main() { let score = 75; // if is an expression in Rust: let grade = if score >= 60 { "pass" } else { "fail" }; println!("{}", grade); }
const score = 75; // if is a statement in TypeScript — use ternary for inline expressions: const grade = score >= 60 ? "pass" : "fail"; console.log(grade); // Or use if/else with a pre-declared variable: let grade2: string; if (score >= 60) { grade2 = "pass"; } else { grade2 = "fail"; } console.log(grade2);
Rust's if is an expression that produces a value; the last expression in each arm is the result. TypeScript's if is a statement — it cannot produce a value inline. Use the ternary operator (condition ? a : b) for simple inline choices, or a pre-declared variable with an if/else block for more complex logic. TypeScript also requires parentheses around the condition (unlike Rust).
Loops — for, while, loop
fn main() { for i in 0..5 { print!("{} ", i); } println!(); let mut counter = 0; while counter < 3 { counter += 1; } println!("counter: {}", counter); let result = loop { counter += 1; if counter == 5 { break counter * 10; } }; println!("loop result: {}", result); }
for (let i = 0; i < 5; i++) { process.stdout.write(i + " "); } console.log(); let counter = 0; while (counter < 3) { counter++; } console.log("counter:", counter); // TypeScript has no loop-as-expression; use a variable: let loopResult = 0; while (true) { counter++; if (counter === 5) { loopResult = counter * 10; break; } } console.log("loop result:", loopResult);
Rust has three loop forms: for over iterators, while for conditions, and loop for infinite loops. TypeScript inherits JavaScript's three: C-style for, while, and do...while. Rust's loop { break value; } can produce a value; TypeScript has no equivalent — use a pre-declared variable. Rust's 0..5 range has no TypeScript counterpart; use for (let i = 0; i < 5; i++).
for...of and for...in iteration
fn main() { let names = vec!["Alice", "Bob", "Carol"]; for name in &names { println!("{}", name); } for (index, name) in names.iter().enumerate() { println!("{}: {}", index, name); } }
const names = ["Alice", "Bob", "Carol"]; for (const name of names) { // for...of: values console.log(name); } names.forEach((name, index) => { // with index console.log(`${index}: ${name}`); }); // for...in iterates KEYS (indices for arrays) — rarely what you want: for (const index in names) { console.log(typeof index, index); // string "0", "1", "2" (not number!) }
TypeScript's for...of iterates values, equivalent to Rust's for item in &collection. Use forEach with a callback when you need the index. Beware for...in: it iterates keys (array indices as strings, object property names) — almost never what you want for arrays. Rust's .iter().enumerate() gives both index and value with correct types; TypeScript's entries() method (for (const [i, v] of names.entries())) does the same.
switch vs match
fn main() { let value = 3; let description = match value { 1 => "one", 2 | 3 => "two or three", 4..=9 => "four to nine", _ => "other", }; println!("{}", description); // Exhaustiveness is checked at compile time. }
const value = 3; let description: string; switch (value) { case 1: description = "one"; break; case 2: case 3: description = "two or three"; break; default: description = "other"; } console.log(description); // TypeScript does NOT check exhaustiveness for number switch — use union types.
Rust's match is exhaustive — the compiler requires all cases to be handled. TypeScript's switch has fall-through by default (requires explicit break) and does not enforce exhaustiveness for number or string values. TypeScript's discriminated unions with a never check provide compile-time exhaustiveness for union types. Both languages support matching multiple values, but Rust uses | while TypeScript uses consecutive case labels.
Functions & Closures
Function definitions
fn add(first: i32, second: i32) -> i32 { first + second // implicit return } fn greet(name: &str) -> String { format!("Hello, {}!", name) } fn main() { println!("{}", add(3, 4)); println!("{}", greet("Alice")); }
function add(first: number, second: number): number { return first + second; // explicit return required } function greet(name: string): string { return `Hello, ${name}!`; } console.log(add(3, 4)); console.log(greet("Alice"));
TypeScript function syntax (function name(param: Type): ReturnType) resembles Rust's (fn name(param: Type) -> ReturnType) but requires an explicit return statement. Rust returns the value of the last expression implicitly if there is no semicolon; TypeScript's only implicit return is from arrow functions with a body-expression (no braces). TypeScript functions are first-class values like Rust functions, but with structural typing rather than nominal function types.
Optional and default parameters
fn greet(name: &str, greeting: Option<&str>) -> String { let greeting = greeting.unwrap_or("Hello"); format!("{}, {}!", greeting, name) } fn main() { println!("{}", greet("Alice", None)); println!("{}", greet("Bob", Some("Hi"))); }
function greet(name: string, greeting: string = "Hello"): string { return `${greeting}, ${name}!`; } console.log(greet("Alice")); // default greeting console.log(greet("Bob", "Hi")); // explicit greeting // Optional parameters (may be undefined): function describe(name: string, age?: number): string { return age !== undefined ? `${name} is ${age}` : name; } console.log(describe("Alice")); console.log(describe("Bob", 30));
TypeScript has built-in syntax for optional parameters (name?: Type, which has type Type | undefined) and default parameters (name: Type = default). Rust has neither — use Option<T> parameters and call .unwrap_or(default) manually. TypeScript optional parameters must come after required parameters; Rust has no such constraint since callers must always pass Some(value) or None explicitly.
Arrow functions and closures
fn apply<F: Fn(i32) -> i32>(operation: F, value: i32) -> i32 { operation(value) } fn main() { let multiplier = 3; let triple = |x| x * multiplier; // captures multiplier println!("{}", apply(triple, 7)); // 21 }
function apply(operation: (x: number) => number, value: number): number { return operation(value); } const multiplier = 3; const triple = (x: number): number => x * multiplier; // captures multiplier console.log(apply(triple, 7)); // 21 // Short arrow function — implicit return when body is an expression: const double = (x: number) => x * 2; console.log(double(5));
TypeScript arrow functions ((x: number) => x * 2) are the idiomatic closure syntax, equivalent to Rust's |x| x * 2. Both capture variables from the enclosing scope. TypeScript arrow functions with a single expression return it implicitly (no braces, no return); with a body ({ }) you must use return explicitly. Unlike Rust's Fn/FnMut/FnOnce distinction, TypeScript closures make no distinction about capture mode.
Rest parameters and spread calls
fn sum(numbers: &[i32]) -> i32 { numbers.iter().sum() } fn main() { println!("{}", sum(&[1, 2, 3, 4, 5])); // slice argument let values = vec![10, 20, 30]; println!("{}", sum(&values)); }
function sum(...numbers: number[]): number { return numbers.reduce((total, x) => total + x, 0); } console.log(sum(1, 2, 3, 4, 5)); // 15 const values = [10, 20, 30]; console.log(sum(...values)); // spread array into call — 60
TypeScript supports true variadic functions with rest parameters (...name: T[]) — the argument arrives as an array. Call a variadic function with a spread: fn(...array). Rust does not have variadic user functions; instead, callers pass a slice (&[T]). TypeScript's spread-call syntax (fn(...arr)) is similar to Rust's slice-passing but more concise. Rust macros like println! and vec! use variadic-like syntax but are resolved at compile time.
Higher-order functions
fn compose<A, B, C>( first: impl Fn(A) -> B, second: impl Fn(B) -> C, ) -> impl Fn(A) -> C { move |input| second(first(input)) } fn main() { let double_then_add_one = compose(|x: i32| x * 2, |x| x + 1); println!("{}", double_then_add_one(5)); // 11 }
function compose<A, B, C>( first: (input: A) => B, second: (input: B) => C, ): (input: A) => C { return (input) => second(first(input)); } const doubleThenAddOne = compose( (x: number) => x * 2, (x: number) => x + 1, ); console.log(doubleThenAddOne(5)); // 11
Both languages support higher-order functions — functions that take or return other functions. TypeScript's function types use arrow notation in signatures: (param: T) => R. Rust uses trait bounds: impl Fn(T) -> R. Rust's returned closure must be wrapped in Box<dyn Fn(T) -> R> or use impl Fn; TypeScript returns a function directly with no boxing. Rust requires move to transfer captured values into a returned closure; TypeScript captures by reference automatically.
Classes & Structs
Classes vs structs
struct Point { x: f64, y: f64, } impl Point { fn new(x: f64, y: f64) -> Self { Self { x, y } } fn distance_from_origin(&self) -> f64 { (self.x * self.x + self.y * self.y).sqrt() } } fn main() { let point = Point::new(3.0, 4.0); println!("{}", point.distance_from_origin()); }
class Point { constructor( readonly x: number, readonly y: number, ) {} distanceFromOrigin(): number { return Math.sqrt(this.x ** 2 + this.y ** 2); } } const point = new Point(3, 4); console.log(point.distanceFromOrigin());
TypeScript classes bundle data and methods together like Rust's struct + impl block, but in one syntax. TypeScript uses new for construction; Rust uses associated functions like Point::new(). TypeScript's readonly in a constructor parameter is shorthand for declaring a property and assigning it in one step. TypeScript classes support inheritance (extends); Rust has no inheritance — use traits and composition instead.
Access modifiers vs pub/private
struct BankAccount { balance: f64, // private by default — only accessible within this module } impl BankAccount { pub fn new(opening_balance: f64) -> Self { BankAccount { balance: opening_balance } } pub fn deposit(&mut self, amount: f64) { self.balance += amount; } pub fn balance(&self) -> f64 { self.balance } } fn main() { let mut account = BankAccount::new(100.0); account.deposit(50.0); println!("{}", account.balance()); // 150 }
class BankAccount { private balance: number; constructor(openingBalance: number) { this.balance = openingBalance; } deposit(amount: number): void { this.balance += amount; } getBalance(): number { return this.balance; } } const account = new BankAccount(100); account.deposit(50); console.log(account.getBalance()); // 150
TypeScript uses private, protected, and public (default) access modifiers. These are compile-time checks only — the JavaScript runtime has no access restriction. Rust's pub / module-private distinction is enforced by the compiler and baked into the module system. TypeScript also has the JavaScript-native #field syntax for truly private fields enforced at runtime.
Inheritance vs traits and composition
trait Speak { fn speak(&self) -> String; } struct Animal { name: String } struct Dog { inner: Animal } impl Dog { fn new(name: &str) -> Self { Dog { inner: Animal { name: name.to_string() } } } } impl Speak for Dog { fn speak(&self) -> String { format!("{} says Woof!", self.inner.name) } } fn make_sound(entity: &dyn Speak) { println!("{}", entity.speak()); } fn main() { make_sound(&Dog::new("Rex")); }
class Animal { constructor(readonly name: string) {} speak(): string { return `${this.name} makes a sound`; } } class Dog extends Animal { speak(): string { return `${this.name} says Woof!`; } // override } function makeSound(entity: Animal): void { console.log(entity.speak()); } makeSound(new Dog("Rex"));
TypeScript supports single-inheritance with extends, just like Java or C#. Rust has no inheritance — the preferred pattern is traits for shared behavior and composition (embedding a struct as a field) for shared structure. TypeScript's polymorphism works through the prototype chain at runtime; Rust's through trait objects (dyn Trait) or monomorphization at compile time. A function that accepts a base class in TypeScript also accepts any subclass — this is the Liskov substitution principle enforced structurally.
Static methods and associated functions
struct Counter { count: u32, } impl Counter { fn new() -> Self { Counter { count: 0 } } // associated function fn increment(&mut self) { self.count += 1; } fn reset() { println!("(Counter does not track global state)"); } } fn main() { let mut counter = Counter::new(); counter.increment(); Counter::reset(); println!("{}", counter.count); }
class Counter { private count: number = 0; static create(): Counter { return new Counter(); } // static factory increment(): void { this.count++; } getCount(): number { return this.count; } static reset(): void { console.log("(Counter does not track global state)"); } } const counter = Counter.create(); counter.increment(); Counter.reset(); console.log(counter.getCount());
TypeScript static methods belong to the class constructor, not to any instance — called as Class.method() with no this referring to an instance. Rust's equivalent is an associated function in an impl block without a &self receiver — called as Type::function(). Both are used for factory functions, utility methods, and constructors. TypeScript static methods can access this where this refers to the class constructor itself.
Interfaces
Interface definitions — structural vs nominal
trait Greet { fn greet(&self) -> String; } struct Person { name: String } // Explicit impl required — nominal typing: impl Greet for Person { fn greet(&self) -> String { format!("Hello, {}!", self.name) } } fn introduce(entity: &dyn Greet) { println!("{}", entity.greet()); } fn main() { let person = Person { name: "Alice".to_string() }; introduce(&person); }
interface Greet { greet(): string; } type Person = { name: string }; // No explicit impl — satisfied implicitly by shape (structural typing): function buildPerson(name: string): Person & Greet { return { name, greet() { return `Hello, ${name}!`; }, }; } function introduce(entity: Greet): void { console.log(entity.greet()); } const person = buildPerson("Alice"); introduce(person);
TypeScript interfaces are satisfied structurally — any object with the required method signatures automatically satisfies the interface, without any declaration. Rust traits require explicit impl Trait for Type — purely nominal. TypeScript's structural typing allows you to pass a plain object literal directly to a function expecting an interface, which is idiomatic. In Rust you must define a named struct and provide the impl block even for simple cases.
Interface extension and composition
trait Animal { fn name(&self) -> &str; } trait Swimmer: Animal { // Swimmer requires Animal fn swim(&self) -> String; } struct Duck { name: String } impl Animal for Duck { fn name(&self) -> &str { &self.name } } impl Swimmer for Duck { fn swim(&self) -> String { format!("{} swims", self.name) } } fn make_swim(swimmer: &dyn Swimmer) { println!("{}", swimmer.swim()); } fn main() { make_swim(&Duck { name: "Donald".to_string() }); }
interface Animal { name: string; } interface Swimmer extends Animal { // extends adds Animal's shape swim(): string; } const duck: Swimmer = { name: "Donald", swim() { return `${this.name} swims`; }, }; function makeSwim(swimmer: Swimmer): void { console.log(swimmer.swim()); } makeSwim(duck);
TypeScript interface extension (interface B extends A) combines the shapes of multiple interfaces — equivalent to Rust's supertrait syntax (trait Swimmer: Animal). TypeScript also supports extending multiple interfaces at once (interface C extends A, B); Rust requires listing all supertraits. Both mechanisms create a "must also satisfy parent" requirement, but TypeScript checks this structurally at usage sites while Rust checks it at the impl site.
Readonly and optional properties
struct Config { host: String, port: u16, timeout: Option<u32>, // optional field } fn main() { let config = Config { host: "localhost".to_string(), port: 8080, timeout: None, }; let timeout = config.timeout.unwrap_or(30); println!("{}:{} timeout={}s", config.host, config.port, timeout); }
interface Config { readonly host: string; // cannot be reassigned after creation readonly port: number; timeout?: number; // optional — may be absent (undefined) } const config: Config = { host: "localhost", port: 8080 }; // config.host = "other"; // TS error: readonly property const timeout = config.timeout ?? 30; console.log(`${config.host}:${config.port} timeout=${timeout}s`);
TypeScript interfaces can mark properties as readonly (cannot be reassigned) or optional (?, may be absent — type is T | undefined). Rust's equivalent is Option<T> for optional values and the borrow checker's immutability rules for read-only access. TypeScript's readonly is a compile-time check only; JavaScript has no runtime enforcement. Readonly<T> and Partial<T> are utility types that make all properties of a type readonly or optional.
Unions & Enums
Union types vs Rust enums
enum Shape { Circle(f64), Rectangle(f64, f64), } fn area(shape: &Shape) -> f64 { match shape { Shape::Circle(radius) => std::f64::consts::PI * radius * radius, Shape::Rectangle(width, height) => width * height, } } fn main() { println!("{:.2}", area(&Shape::Circle(5.0))); println!("{:.2}", area(&Shape::Rectangle(3.0, 4.0))); }
type Shape = | { kind: "circle"; radius: number } | { kind: "rectangle"; width: number; height: number }; function area(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "rectangle": return shape.width * shape.height; } } console.log(area({ kind: "circle", radius: 5 }).toFixed(2)); console.log(area({ kind: "rectangle", width: 3, height: 4 }).toFixed(2));
TypeScript's discriminated union pattern — a union of object types that share a kind (or type) string literal field — is the closest equivalent to Rust enums with data. TypeScript narrows the type within each case branch based on the discriminant value. The key difference: Rust exhaustiveness is a compile-time guarantee; TypeScript switch only checks exhaustiveness for unions if you add a never check in the default case.
Type narrowing
fn describe(value: &dyn std::any::Any) -> &'static str { if value.downcast_ref::<i32>().is_some() { return "integer"; } if value.downcast_ref::<String>().is_some() { return "string"; } "other" } fn main() { println!("{}", describe(&42_i32)); println!("{}", describe(&"hello".to_string())); }
function describe(value: number | string | boolean): string { if (typeof value === "number") { return `number: ${value.toFixed(2)}`; // TypeScript knows: value is number here } if (typeof value === "string") { return `string: ${value.toUpperCase()}`; // TypeScript knows: value is string here } return `boolean: ${value}`; // TypeScript knows: value is boolean here } console.log(describe(3.14)); console.log(describe("hello")); console.log(describe(true));
TypeScript type narrowing uses runtime checks (typeof, instanceof, the in operator, or a discriminant field check) to refine a union type within a conditional block. Rust uses pattern matching with match or if let for the same purpose. TypeScript's narrowing is flow-based — the compiler tracks which branch you are in and adjusts the type accordingly. This is a purely compile-time analysis; the runtime is plain JavaScript with no type information.
TypeScript const enums — not sum types
// Rust enums are algebraic data types — each variant can carry data. // Use them as a tagged union with associated values: #[derive(Debug)] enum Direction { North, South, East, West } fn opposite(direction: Direction) -> Direction { match direction { Direction::North => Direction::South, Direction::South => Direction::North, Direction::East => Direction::West, Direction::West => Direction::East, } } fn main() { println!("{:?}", opposite(Direction::North)); }
// TypeScript 'enum' is a numeric/string constant set — not a sum type. const enum Direction { North, South, East, West } function opposite(direction: Direction): Direction { switch (direction) { case Direction.North: return Direction.South; case Direction.South: return Direction.North; case Direction.East: return Direction.West; case Direction.West: return Direction.East; } } console.log(opposite(Direction.North)); // 1 (South's numeric value) // For sum types with data, use discriminated unions (see previous concept).
TypeScript's enum (and const enum) is a set of named constants — not a sum type. Each member is a number or string with no associated data. This is far less powerful than Rust's enums, which are algebraic data types where each variant can carry arbitrary data. For the Rust enum pattern with data, use discriminated unions (object types sharing a literal kind field). const enum is inlined to its literal value by the TypeScript compiler, producing no runtime object.
Exhaustiveness checking with never
enum Color { Red, Green, Blue } fn color_name(color: Color) -> &'static str { match color { Color::Red => "red", Color::Green => "green", Color::Blue => "blue", // Forgetting a variant is a compile error. } } fn main() { println!("{}", color_name(Color::Green)); }
type Color = "red" | "green" | "blue"; function colorName(color: Color): string { switch (color) { case "red": return "red"; case "green": return "green"; case "blue": return "blue"; default: { // If all cases are handled, color is 'never' here. // If you add a new variant to Color, this line becomes a type error: const exhaustivenessCheck: never = color; throw new Error(`Unhandled color: ${exhaustivenessCheck}`); } } } console.log(colorName("green"));
Rust's match enforces exhaustiveness at compile time — you must handle every variant. TypeScript does not enforce this by default for switch. The never trick adds it: in the default branch, assign color to a never variable — if all cases are covered, color narrows to never and the assignment is valid; if a case is missing, color still has that type, and assigning it to never is a type error. This is a compile-time guard only.
Generics
Generic functions
fn first<T>(slice: &[T]) -> Option<&T> { slice.first() } fn main() { let numbers = vec![10, 20, 30]; if let Some(&value) = first(&numbers) { println!("{}", value); } let words = vec!["alpha", "beta"]; if let Some(&word) = first(&words) { println!("{}", word); } }
function first<T>(array: T[]): T | undefined { return array[0]; } const numbers = [10, 20, 30]; const number = first(numbers); if (number !== undefined) { console.log(number); } const words = ["alpha", "beta"]; const word = first(words); if (word !== undefined) { console.log(word); }
TypeScript generics use angle brackets: function f<T>(x: T): T. Rust uses the same syntax. A key difference: TypeScript generics are erased at runtime — there is no way to query the type parameter at runtime (no TypeId equivalent for generics). Rust generics are monomorphized — the compiler generates a separate concrete version for each type, which means the type is always known. TypeScript returns T | undefined where Rust returns Option<&T>.
Generic constraints — extends vs trait bounds
use std::fmt::Display; fn print_largest<T: PartialOrd + Display>(items: &[T]) { let mut largest = &items[0]; for item in items { if item > largest { largest = item; } } println!("largest: {}", largest); } fn main() { print_largest(&[3, 1, 4, 1, 5, 9]); print_largest(&["apple", "zebra", "mango"]); }
// 'extends' constrains the type parameter to a supertype: function printLargest<T extends number | string>(items: T[]): void { const largest = items.reduce((max, item) => item > max ? item : max); console.log("largest:", largest); } printLargest([3, 1, 4, 1, 5, 9]); printLargest(["apple", "zebra", "mango"]); // Constrain to objects with a .length property: function logLength<T extends { length: number }>(item: T): void { console.log("length:", item.length); } logLength("hello"); logLength([1, 2, 3]);
TypeScript uses extends to constrain a type parameter: T extends SomeType means T must be assignable to SomeType. Rust uses trait bounds: T: PartialOrd + Display. TypeScript's structural approach allows ad-hoc constraints like T extends { length: number } — any type with a length property matches, without a named interface. Rust requires implementing the named trait; there is no structural "duck typing" at the trait level.
Generic type aliases and interfaces
struct Stack<T> { items: Vec<T>, } impl<T> Stack<T> { fn new() -> Self { Stack { items: Vec::new() } } fn push(&mut self, item: T) { self.items.push(item); } fn pop(&mut self) -> Option<T> { self.items.pop() } fn is_empty(&self) -> bool { self.items.is_empty() } } fn main() { let mut stack: Stack<i32> = Stack::new(); stack.push(1); stack.push(2); println!("{:?}", stack.pop()); // Some(2) }
class Stack<T> { private items: T[] = []; push(item: T): void { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } isEmpty(): boolean { return this.items.length === 0; } } const stack = new Stack<number>(); stack.push(1); stack.push(2); console.log(stack.pop()); // 2
TypeScript and Rust use almost identical syntax for generic classes/structs: class Stack<T> vs struct Stack<T>. TypeScript generic classes carry the type parameter to all methods automatically; Rust requires impl<T> Stack<T> to reintroduce the parameter on the impl block. TypeScript generic type parameters can have defaults (T = string); Rust supports this too (T = DefaultType). TypeScript type aliases can also be generic: type Pair<T> = [T, T].
null/undefined vs Option
null/undefined vs Option<T>
fn find_user(identifier: u32) -> Option<String> { if identifier == 1 { Some("Alice".to_string()) } else { None } } fn main() { match find_user(1) { Some(name) => println!("found: {}", name), None => println!("not found"), } // The compiler forces you to handle the None case. }
function findUser(identifier: number): string | undefined { if (identifier === 1) return "Alice"; return undefined; } // TypeScript does NOT force you to check for undefined — easy to forget! const user = findUser(1); if (user !== undefined) { console.log("found:", user); } else { console.log("not found"); } // With strictNullChecks enabled, TypeScript catches many (but not all) unsafe uses: const upper = user?.toUpperCase(); // safe — optional chaining console.log(upper);
TypeScript has two nil-like values: null (explicit empty) and undefined (absent/uninitialized). With strictNullChecks: true in tsconfig.json, TypeScript tracks which values might be null or undefined and prevents calling methods on them without a check. Rust's Option<T> is a compile-time algebraic type — the compiler makes it impossible to use the inner value without pattern matching or an explicit unwrap. TypeScript's approach is more permissive.
Nullish coalescing and optional chaining
struct Config { timeout: Option<u32> } fn main() { let config = Config { timeout: None }; // unwrap_or for default: let timeout = config.timeout.unwrap_or(30); println!("timeout: {}s", timeout); // map for safe transformation: let message = config.timeout.map(|secs| format!("{}s", secs)); println!("{:?}", message); // None }
type Config = { timeout?: number }; const config: Config = {}; // ?? returns the right side if left is null or undefined: const timeout = config.timeout ?? 30; console.log("timeout:", timeout + "s"); // ?. short-circuits to undefined if the receiver is null/undefined: const nested: { inner?: { value?: string } } = {}; const value = nested.inner?.value ?? "default"; console.log(value);
TypeScript's ?? nullish coalescing operator is equivalent to Rust's .unwrap_or(default): it returns the right operand if the left is null or undefined. Note that ?? only triggers on null/undefined, not on 0 or "" (unlike ||). Optional chaining (?.) short-circuits to undefined and corresponds to Rust's .as_ref().map(|v| &v.field) pattern on nested Options.
Non-null assertion operator (!)
fn main() { // Rust: unwrap() panics at runtime if None — explicit opt-in to panic. let maybe: Option<i32> = Some(42); let value = maybe.unwrap(); // panics if None println!("{}", value); // Better: use expect() for a meaningful panic message: let value2 = maybe.expect("value should have been set at startup"); println!("{}", value2); }
function findValue(): string | undefined { return "hello"; } const result = findValue(); // The ! non-null assertion tells TypeScript "trust me, this is not null/undefined". // It does NOTHING at runtime — no check, no error, just removes undefined from the type. const upper = result!.toUpperCase(); console.log(upper); // If result were actually undefined, this would throw at runtime with no TS error. // Prefer explicit checks or optional chaining over ! when possible.
TypeScript's ! postfix operator is a type-system assertion: it removes null and undefined from the inferred type without any runtime check. Rust's .unwrap() is similar in spirit but panics at runtime if the value is None — it is explicit and auditable. TypeScript's ! produces no runtime behavior at all, which makes bugs harder to find. Use it only when you have external knowledge that the compiler cannot verify, and document why.
Error Handling
Result<T,E> vs throw/catch
use std::num::ParseIntError; fn parse_port(input: &str) -> Result<u16, ParseIntError> { let number: u16 = input.parse()?; Ok(number) } fn main() { match parse_port("8080") { Ok(port) => println!("port: {}", port), Err(error) => println!("error: {}", error), } }
function parsePort(input: string): number { const number = parseInt(input, 10); if (isNaN(number) || number < 0 || number > 65535) { throw new Error(`Invalid port: ${input}`); } return number; } try { console.log("port:", parsePort("8080")); console.log("port:", parsePort("invalid")); } catch (error) { if (error instanceof Error) { console.log("error:", error.message); } }
TypeScript uses exceptions (throw/try/catch) for error handling — there is no equivalent to Rust's Result<T, E> in the standard library. Exceptions are invisible in function signatures: you cannot tell from function f(): number whether f might throw. Rust's Result<T, E> makes errors explicit in the return type. Third-party libraries like neverthrow bring Result-style error handling to TypeScript, but the ecosystem default is exceptions.
Custom error types
use std::fmt; #[derive(Debug)] struct AppError { code: u32, message: String } impl fmt::Display for AppError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!(formatter, "error {}: {}", self.code, self.message) } } impl std::error::Error for AppError {} fn main() { let error = AppError { code: 404, message: "not found".to_string() }; println!("{}", error); }
class AppError extends Error { constructor( readonly code: number, message: string, ) { super(message); this.name = "AppError"; // shows the class name in error messages } } function riskyOperation(): never { throw new AppError(404, "not found"); } try { riskyOperation(); } catch (error) { if (error instanceof AppError) { console.log(`error ${error.code}: ${error.message}`); } }
TypeScript custom errors extend the built-in Error class. Call super(message) to initialize the message property and set this.name to the class name for readable stack traces. Rust custom errors implement Display and std::error::Error. TypeScript's catch (error) always catches type unknown (in strict mode) — use instanceof to narrow it. The never return type means the function always throws and never returns normally.
Error propagation — ? vs re-throw
fn parse_and_double(input: &str) -> Result<i32, std::num::ParseIntError> { let number: i32 = input.parse()?; // ? propagates the error Ok(number * 2) } fn main() { match parse_and_double("21") { Ok(value) => println!("{}", value), Err(error) => println!("error: {}", error), } }
function parseAndDouble(input: string): number { const number = parseInt(input, 10); if (isNaN(number)) throw new Error(`Not a number: ${input}`); return number * 2; } // Callers must wrap in try/catch to propagate — no ? operator: function processInput(input: string): void { try { const result = parseAndDouble(input); console.log(result); } catch (error) { throw new Error(`processInput failed: ${error}`); } } processInput("21");
Rust's ? operator propagates an error up the call stack in one character. TypeScript has no equivalent — to propagate, you either re-throw inside a catch block or let the exception propagate automatically (since any uncaught throw propagates). Automatic propagation is simpler but hides the error path from the reader. Some TypeScript teams use a Result pattern: function f(): { ok: true; value: T } | { ok: false; error: E } — verbose but explicit.
Async & Concurrency
async/await — event loop vs futures
// Rust async requires an executor (tokio, async-std). // This example uses std::thread::sleep for a sync delay instead: use std::thread; use std::time::Duration; fn main() { println!("start"); thread::sleep(Duration::from_millis(10)); println!("done"); }
async function fetchData(): Promise<string> { // Simulate async work with a Promise: await new Promise<void>(resolve => setTimeout(resolve, 10)); return "data received"; } async function main(): Promise<void> { console.log("start"); const data = await fetchData(); console.log(data); console.log("done"); } main();
TypeScript async functions return a Promise<T> implicitly. await suspends the current function until the Promise resolves. The entire event loop runs on a single thread — no true parallelism for CPU-bound work. Rust's async fn returns an impl Future<Output = T> that does nothing until polled; an async runtime (Tokio, async-std) drives execution across multiple threads. Top-level await is available in TypeScript modules but not in classic scripts — wrap in an async function for compatibility.
Running async tasks concurrently
// Rust tokio::join! runs multiple futures concurrently: // use tokio; // #[tokio::main] // async fn main() { // let (a, b) = tokio::join!(fetch_a(), fetch_b()); // println!("{} {}", a, b); // } // (Cannot run without tokio — marked norun.) fn main() { println!("(async example requires tokio runtime)"); }
async function fetchValue(label: string, delayMs: number): Promise<string> { await new Promise<void>(resolve => setTimeout(resolve, delayMs)); return `${label} done`; } async function main(): Promise<void> { // Promise.all runs both concurrently — total time ≈ max(10, 20) not 10+20: const [resultA, resultB] = await Promise.all([ fetchValue("A", 10), fetchValue("B", 20), ]); console.log(resultA, resultB); // Promise.allSettled waits for all, even if some reject: const results = await Promise.allSettled([ Promise.resolve("ok"), Promise.reject(new Error("fail")), ]); results.forEach(result => console.log(result.status)); } main();
Promise.all takes an array of Promises and resolves when all complete — equivalent to Rust's tokio::join!. If any Promise rejects, Promise.all rejects immediately with that error (short-circuits). Promise.allSettled waits for all and reports each as { status: "fulfilled", value } or { status: "rejected", reason }, never short-circuiting — equivalent to Rust's join_all collecting Results. Promise.race resolves with the first to settle.
Iterators and generators
fn fibonacci() -> impl Iterator<Item = u64> { let (mut previous, mut current) = (0_u64, 1_u64); std::iter::from_fn(move || { let next = previous + current; previous = current; current = next; Some(previous) }) } fn main() { for value in fibonacci().take(8) { print!("{} ", value); } println!(); }
function* fibonacci(): Generator<number> { let [previous, current] = [0, 1]; while (true) { yield current; [previous, current] = [current, previous + current]; } } const fib = fibonacci(); const first8 = Array.from({ length: 8 }, () => fib.next().value); console.log(first8.join(" "));
TypeScript generators (function*) produce a sequence of values lazily using yield, similar to Rust's Iterator trait and std::iter::from_fn. Both produce values on demand without materializing the entire sequence. Rust iterators are more composable — chains of .map(), .filter(), and .take() work seamlessly on any Iterator. TypeScript generators return a Generator object with a .next() method; you can spread them with for...of or Array.from.
Concurrency model — single thread vs multi-thread
// Rust: true parallelism with OS threads (no GIL, no event loop). use std::thread; use std::sync::{Arc, Mutex}; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..4 { let counter = Arc::clone(&counter); handles.push(thread::spawn(move || { let mut guard = counter.lock().unwrap(); *guard += 1; })); } for handle in handles { handle.join().unwrap(); } println!("{}", *counter.lock().unwrap()); // 4 }
// TypeScript (Node.js): single-threaded event loop. // CPU-parallelism requires Worker Threads (Node.js) or Web Workers (browser). // For I/O-bound concurrency, Promise.all is sufficient — no threads needed. async function simulateIoWork(identifier: number): Promise<number> { await new Promise<void>(resolve => setTimeout(resolve, 5)); return identifier * 2; } async function main(): Promise<void> { // Run 4 tasks "concurrently" on a single thread: const results = await Promise.all([1, 2, 3, 4].map(simulateIoWork)); const total = results.reduce((sum, x) => sum + x, 0); console.log("total:", total); // 20 } main();
TypeScript (in Node.js or browsers) runs on a single thread. Promise.all runs tasks concurrently by interleaving them on the event loop — they do not run in parallel. This is fine for I/O-bound work (network, disk) but cannot speed up CPU-bound work. Rust threads use multiple CPU cores genuinely. For CPU parallelism in TypeScript, use Worker Threads (Node.js) or Web Workers (browser). The trade-off: TypeScript has no data races by design (one thread); Rust prevents them through the borrow checker and Send/Sync traits.