PONY λ M2 Modula-2

TypeScript.CodeCompared.To/Ruby

An interactive executable cheatsheet comparing TypeScript and Ruby

TypeScript 6.0 Ruby 4.0
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.