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.classIn 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.classRuby
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 mutableIn 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 % 2Unlike 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 bigRuby
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}"
endBecause 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.lengthArray 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, totalThe 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}"
endA 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 countsRuby 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 greetingIn 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
putsRuby
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
endA 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.distanceThe 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.valueBecause 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.speakRuby 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").greetA 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"
endRuby 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"
endYou 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)
endCustom 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 letterRuby'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})"
endRuby 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
endArray 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.anythingWhere 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.oneClasses 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.