Hello World & Running
Hello, World
console.log("Hello, World!"); puts "Hello, World!" Ruby has no
console object — puts ("put string") writes a value followed by a newline. There is no semicolon and no surrounding boilerplate; top-level code runs the moment the file loads, exactly like a TypeScript script run through tsx.Comments
// a line comment
/* a block comment */
console.log("done"); # a line comment
=begin
a block comment
=end
puts "done" Ruby uses
# for line comments instead of //. Block comments are the rarely-used =begin/=end pair, which must start in column zero — most Rubyists simply prefix each line with #.String interpolation
const name = "Ada";
console.log(`Hello, ${name}!`); name = "Ada"
puts "Hello, #{name}!" Ruby interpolates with
#{...} inside double-quoted strings — no backtick template literal needed. Single-quoted strings, like '...', are literal and do not interpolate.Variables & Types
Declaring variables
let count: number = 3;
const label: string = "items";
count = count + 1;
console.log(`${count} ${label}`); count = 3
label = "items"
count = count + 1
puts "#{count} #{label}" Ruby has no
let, const, or type annotations. A bare assignment creates a local variable, and there is no compile step — types are checked only at runtime. Convention, not a keyword, signals intent: an UPPER_CASE name is a constant and Ruby warns when you reassign it.Everything is an object
console.log(typeof 42);
console.log((42).toString());
console.log(typeof null); puts 42.class
puts 42.to_s
puts nil.class In Ruby every value is a real object with a class, including integers and
nil — there is no primitive/object split as in TypeScript. 42.class is Integer and you can call methods directly on literals.nil vs null/undefined
const value: string | null = null;
console.log(value ?? "fallback");
console.log(value === null); value = nil
puts value || "fallback"
puts value.nil? Ruby has a single absent value,
nil — there is no undefined. Only nil and false are falsy (zero and the empty string are truthy!), so value || "fallback" behaves like the ?? operator for nil.Symbols
const status = "active";
console.log(status === "active"); status = :active
puts status == :active
puts status.class Ruby
Symbols (written :name) are interned, immutable identifiers with no direct TypeScript equivalent — think of them as lightweight, reusable label values used for hash keys and method names rather than as strings.Strings
Common string methods
const text = "Hello, World";
console.log(text.toUpperCase());
console.log(text.length);
console.log(text.includes("World")); text = "Hello, World"
puts text.upcase
puts text.length
puts text.include?("World") Ruby method names are
snake_case rather than camelCase. Predicate methods that return a boolean conventionally end in ?, so include? reads like a question.Frozen string literals
let greeting = "Hi";
greeting += ", there";
console.log(greeting); greeting = "Hi"
greeting += ", there"
puts greeting
mutable = +"Hi"
mutable << ", there"
puts mutable In Ruby 4.0 string literals are frozen (immutable) by default — closer to TypeScript's value semantics for
string. Reassignment with += always works because it makes a new string, but in-place mutation with << requires an explicitly unfrozen copy, written +"...".Split and join
const csv = "a,b,c";
const parts = csv.split(",");
console.log(parts.join(" | ")); csv = "a,b,c"
parts = csv.split(",")
puts parts.join(" | ") These map almost one to one. The difference is that Ruby
split/join are defined on the objects themselves and chain naturally with the rest of Ruby's enumerable methods.Numbers
Integers vs floats
console.log(7 / 2);
console.log(Math.floor(7 / 2));
console.log(7 % 2); puts 7 / 2
puts 7.fdiv(2)
puts 7 % 2 Unlike TypeScript's single
number type, Ruby has distinct Integer and Float types. Integer division 7 / 2 truncates to 3; use fdiv (or a float literal like 7.0) to get 3.5.Arbitrary-precision integers
const big = 2n ** 100n;
console.log(big.toString()); big = 2 ** 100
puts big Ruby
Integer grows to arbitrary precision automatically — there is no separate BigInt type or n suffix. 2 ** 100 just works and stays an ordinary Integer.Numeric iteration
for (let index = 0; index < 3; index++) {
console.log(`tick ${index}`);
} 3.times do |index|
puts "tick #{index}"
end Because integers are objects, Ruby lets you call iteration methods directly on them.
3.times yields 0, 1, 2 to the block — far more common in idiomatic Ruby than a C-style for loop.Arrays
Array basics
const numbers: number[] = [1, 2, 3];
numbers.push(4);
console.log(numbers[0]);
console.log(numbers.length); numbers = [1, 2, 3]
numbers.push(4)
puts numbers[0]
puts numbers.length Array literals and indexing look identical. Ruby arrays are untyped and can hold mixed values, and they expose a much larger built-in method set than a JavaScript array.
map, filter, reduce
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const total = numbers.reduce((sum, n) => sum + n, 0);
console.log(doubled, evens, total); numbers = [1, 2, 3, 4]
doubled = numbers.map { |number| number * 2 }
evens = numbers.select { |number| number.even? }
total = numbers.reduce(0) { |sum, number| sum + number }
p doubled, evens, total The trio is the same idea, but Ruby spells
filter as select (or filter, an alias) and reduce takes the seed first. Blocks replace arrow functions; p prints an inspectable representation, handy for arrays.Negative indexing & slices
const items = [10, 20, 30, 40];
console.log(items.at(-1));
console.log(items.slice(1, 3)); items = [10, 20, 30, 40]
puts items[-1]
p items[1, 2] Ruby supports negative indices directly with the subscript operator —
items[-1] is the last element. The two-argument form items[start, length] returns a slice, an idiom with no direct TypeScript counterpart.Objects & Hashes
Hashes vs object literals
const person = { name: "Ada", age: 36 };
console.log(person.name);
console.log(person["age"]); person = { name: "Ada", age: 36 }
puts person[:name]
puts person[:age] A Ruby
Hash is the everyday key/value container, written like an object literal. Keys are usually symbols, so you index with person[:name]. There is no dot access — a hash is data, not a struct with named fields.Iterating a hash
const scores = { alice: 90, bob: 75 };
for (const [name, score] of Object.entries(scores)) {
console.log(`${name}: ${score}`);
} scores = { alice: 90, bob: 75 }
scores.each do |name, score|
puts "#{name}: #{score}"
end A Ruby
Hash is directly enumerable — no Object.entries wrapper is needed. The block receives the key and value as separate parameters, and iteration order is the insertion order, just like a JavaScript object.Default values
const counts: Record<string, number> = {};
for (const letter of "banana") {
counts[letter] = (counts[letter] ?? 0) + 1;
}
console.log(counts); counts = Hash.new(0)
"banana".each_char do |letter|
counts[letter] += 1
end
p counts Ruby hashes can carry a default value:
Hash.new(0) returns 0 for any missing key, so the ?? 0 guard disappears entirely. This makes frequency counting a one-liner.Control Flow
if is an expression
const hour = 14;
const greeting = hour < 12 ? "morning" : "afternoon";
console.log(greeting); hour = 14
greeting = if hour < 12
"morning"
else
"afternoon"
end
puts greeting In Ruby
if is an expression that returns its last evaluated branch, so you can assign its result directly. The ternary ? : exists too, but a full if is idiomatic and readable for multi-line branches.Statement modifiers
const items = [1, 2, 3];
if (items.length > 0) {
console.log("not empty");
} items = [1, 2, 3]
puts "not empty" unless items.empty? Ruby lets you append
if or unless to the end of a single statement, reading like English. unless is simply if not, and it pairs nicely with predicate methods like empty?.Ranges
for (let index = 1; index <= 5; index++) {
process.stdout.write(index + " ");
}
console.log(); (1..5).each do |index|
print "#{index} "
end
puts Ruby
Range objects (1..5 inclusive, 1...5 exclusive) are first-class values you can iterate, slice with, or test membership against. They replace most counting for loops.Functions & Methods
Defining methods
function add(a: number, b: number): number {
return a + b;
}
console.log(add(2, 3)); def add(a, b)
a + b
end
puts add(2, 3) Methods are defined with
def and the last expression is returned implicitly — an explicit return is rare. There are no type annotations on the parameters; the method works for any arguments that respond to +.One-line methods
const square = (x: number): number => x * x;
console.log(square(5)); def square(x) = x * x
puts square(5) Ruby 3.0+ added "endless" method definitions —
def name(args) = expression — for one-liners, the closest analogue to a concise arrow function while still being a named method.Keyword arguments
function greet(opts: { name: string; greeting?: string }) {
return `${opts.greeting ?? "Hello"}, ${opts.name}`;
}
console.log(greet({ name: "Ada" })); def greet(name:, greeting: "Hello")
"#{greeting}, #{name}"
end
puts greet(name: "Ada") Where TypeScript fakes named parameters with an options object, Ruby has real keyword arguments. A trailing colon (
name:) makes one required; greeting: "Hello" gives a default. Callers pass them by name in any order.Variadic (splat) arguments
function sum(...values: number[]): number {
return values.reduce((total, value) => total + value, 0);
}
console.log(sum(1, 2, 3, 4)); def sum(*values)
values.reduce(0) { |total, value| total + value }
end
puts sum(1, 2, 3, 4) Ruby's splat operator
* collects extra positional arguments into an array, the same role as TypeScript's rest parameter .... There is a double splat ** for collecting keyword arguments into a hash, too.Blocks & Closures
Blocks
[1, 2, 3].forEach(value => {
console.log(value * 10);
}); [1, 2, 3].each do |value|
puts value * 10
end A block is Ruby's signature feature: an anonymous chunk of code passed to a method between
do...end (or braces). It fills the role of a callback but is lighter-weight syntactically — most iteration methods take a block instead of a function argument.yield
function twice(action: () => void): void {
action();
action();
}
twice(() => console.log("hi")); def twice
yield
yield
end
twice { puts "hi" } Any method can accept a block implicitly and invoke it with
yield — no parameter needs to be declared. This is how Ruby builds custom iterators and resource-management helpers without passing callbacks explicitly.Lambdas & procs
const multiplier = (factor: number) => (x: number) => x * factor;
const triple = multiplier(3);
console.log(triple(10)); multiplier = ->(factor) { ->(x) { x * factor } }
triple = multiplier.call(3)
puts triple.call(10) For a closure you can store in a variable, Ruby has lambdas, written
->(args) { ... }. You invoke them with .call (or the shorthand triple.(10)), unlike a block, which is not an object you can hold onto.Symbol-to-proc shorthand
const words = ["hi", "there"];
console.log(words.map(word => word.toUpperCase())); words = ["hi", "there"]
p words.map(&:upcase) The
&:method shorthand turns a symbol into a block that calls that method on each element — map(&:upcase) replaces map { |word| word.upcase }. It is one of the most common idioms you will read in Ruby code.Classes & Objects
Defining a class
class Point {
constructor(public x: number, public y: number) {}
distance(): number {
return Math.hypot(this.x, this.y);
}
}
const point = new Point(3, 4);
console.log(point.distance()); class Point
def initialize(x, y)
@x = x
@y = y
end
def distance
Math.hypot(@x, @y)
end
end
point = Point.new(3, 4)
puts point.distance The constructor is named
initialize and you instantiate with Point.new, not a new keyword. Instance variables are prefixed with @ and are always private — there is no this. and no field declaration list.Accessors
class Counter {
value = 0;
}
const counter = new Counter();
counter.value = 5;
console.log(counter.value); class Counter
attr_accessor :value
def initialize
@value = 0
end
end
counter = Counter.new
counter.value = 5
puts counter.value Because instance variables are private, Ruby generates getter/setter methods on request with
attr_accessor (or attr_reader for read-only). What looks like field access, counter.value =, is actually a method call to a generated setter.Inheritance
class Animal {
speak(): string { return "..."; }
}
class Dog extends Animal {
override speak(): string { return "Woof"; }
}
console.log(new Dog().speak()); class Animal
def speak = "..."
end
class Dog < Animal
def speak = "Woof"
end
puts Dog.new.speak Ruby uses
< for inheritance instead of extends. Method overriding needs no keyword — a subclass method with the same name simply replaces the parent's, and super calls back up the chain.Duck typing
interface Quacker { quack(): string; }
function makeItQuack(thing: Quacker): string {
return thing.quack();
}
console.log(makeItQuack({ quack: () => "Quack!" })); def make_it_quack(thing)
thing.quack
end
duck = Object.new
def duck.quack = "Quack!"
puts make_it_quack(duck) Ruby has no
interface declarations — it relies on duck typing. Any object that responds to quack is acceptable, checked at the moment of the call. The type is the set of messages an object answers, not a declared contract.Modules & Mixins
Mixins
const Greetable = {
greet(this: { name: string }): string {
return `Hi, I am ${this.name}`;
},
};
const user = Object.assign({ name: "Ada" }, Greetable);
console.log(user.greet()); module Greetable
def greet = "Hi, I am #{name}"
end
class User
include Greetable
attr_reader :name
def initialize(name) = @name = name
end
puts User.new("Ada").greet A Ruby
module bundles methods that you mix into a class with include. This is composition without inheritance — the cleaner cousin of Object.assign, and the mechanism behind Comparable and Enumerable.Namespacing
namespace Geometry {
export const PI = 3.14159;
export function area(r: number): number { return PI * r * r; }
}
console.log(Geometry.area(2)); module Geometry
PI = 3.14159
def self.area(radius) = PI * radius * radius
end
puts Geometry.area(2) Modules double as namespaces, like a TypeScript
namespace. Constants and self. module methods are reached with the scope operator — Geometry::PI for the constant, Geometry.area for the method.Error Handling
try/catch → begin/rescue
try {
throw new Error("boom");
} catch (error) {
console.log((error as Error).message);
} finally {
console.log("cleanup");
} begin
raise "boom"
rescue => error
puts error.message
ensure
puts "cleanup"
end Ruby spells the keywords
begin/rescue/ensure instead of try/catch/finally, and raises with raise rather than throw. A bare raise "msg" creates a RuntimeError for you.Rescuing specific errors
function parse(text: string): number {
const value = Number(text);
if (Number.isNaN(value)) throw new TypeError("not a number");
return value;
}
try {
parse("abc");
} catch (error) {
if (error instanceof TypeError) console.log("bad input");
} def parse(text)
Integer(text)
end
begin
parse("abc")
rescue ArgumentError
puts "bad input"
end You name the exception class after
rescue to catch only that type, the equivalent of an instanceof check in a catch block. Unmatched exceptions propagate up the call stack as usual.Custom exceptions
class ValidationError extends Error {}
try {
throw new ValidationError("invalid");
} catch (error) {
console.log(error instanceof ValidationError);
} class ValidationError < StandardError; end
begin
raise ValidationError, "invalid"
rescue ValidationError => error
puts error.is_a?(ValidationError)
end Custom exceptions subclass
StandardError (not the root Exception, which also covers signals you usually want to leave alone). The ; end on one line is a common way to write an empty class body.Pattern Matching
case/when
const grade = 85;
let letter: string;
switch (true) {
case grade >= 90: letter = "A"; break;
case grade >= 80: letter = "B"; break;
default: letter = "C";
}
console.log(letter); grade = 85
letter = case grade
when 90.. then "A"
when 80...90 then "B"
else "C"
end
puts letter Ruby's
case tests each when with the === operator, which a Range defines as "includes" — so when 80...90 matches any grade in that band. No break is needed; there is no fall-through.Structural pattern matching
const data = { type: "point", x: 1, y: 2 };
if (data.type === "point") {
const { x, y } = data;
console.log(`(${x}, ${y})`);
} data = { type: "point", x: 1, y: 2 }
case data
in { type: "point", x:, y: }
puts "(#{x}, #{y})"
end Ruby 3's
case/in does real structural pattern matching: it matches the shape of the hash and binds x and y in one step. The literal type: "point" must match for the branch to fire — destructuring and a guard combined.Array patterns
const row = [1, 2, 3, 4];
const [head, ...tail] = row;
console.log(head, tail); row = [1, 2, 3, 4]
case row
in [head, *tail]
p head, tail
end Array patterns bind a head and a splat tail just like a TypeScript rest destructure, but inside
case/in they can also assert structure — for example in [Integer, *] only matches when the first element is an integer.Metaprogramming
Dynamic dispatch with send
const text = "hello";
const methodName = "toUpperCase" as const;
console.log(text[methodName]()); text = "hello"
method_name = :upcase
puts text.send(method_name) Ruby's
send calls a method chosen at runtime from a symbol — the disciplined version of TypeScript's bracket-access trick, and it can reach private methods too. It is the backbone of Ruby's dynamic libraries.method_missing
const proxy = new Proxy({} as Record<string, () => string>, {
get: (_target, name) => () => `called ${String(name)}`,
});
console.log(proxy.anything()); class Ghost
def method_missing(name, *args)
"called #{name}"
end
def respond_to_missing?(name, include_private = false) = true
end
puts Ghost.new.anything Where TypeScript reaches for a
Proxy, Ruby has method_missing built into every object: override it to intercept calls to undefined methods. Pair it with respond_to_missing? so the object honestly reports what it can handle.Defining methods at runtime
const obj: Record<string, () => number> = {};
for (const name of ["one", "two"]) {
obj[name] = () => name.length;
}
console.log(obj.one()); class Widget
["one", "two"].each do |name|
define_method(name) { name.length }
end
end
puts Widget.new.one Classes are open and executable in Ruby:
define_method creates real methods while the class body runs, so you can generate an API from data. This is how libraries like Rails conjure dozens of methods from a schema.