The First Day#
I opened a Ruby file for the first time and saw this:
def full_name
"\#{first_name} \#{last_name}"
end
No return. No type annotation. No semicolons. My TypeScript brain immediately asked “where’s the rest of it?” And that was the first lesson: Ruby is a language that trusts you to say less.
I picked up Ruby on Rails out of curiosity — I follow DHH and his Omarchy project, and as someone already running Arch with NeoVim , I figured I would take a look at his other projects. This series documents what it’s like coming from TypeScript and Java.
Implicit Returns (The Biggest Mindset Shift)#
In TypeScript, you write return. In Ruby, the last expression in a method is returned automatically.
# Ruby — last expression is returned
def add(a, b)
a + b
end
# TypeScript — must be explicit
function add(a: number, b: number): number {
return a + b;
}
This felt wrong at first. Where’s the intent? How do I know what’s being returned? But after a week, explicit return started feeling like writing console.log("returning from function") before every return — technically correct, practically noise.
Ruby uses return only for early exits:
def process(order)
return if order.nil? # early exit
return unless order.valid? # guard clause
# ... process the order
order.total # implicit return of the final value
end
0 Is Truthy (Yes, Really)#
This one burned me. In TypeScript and Java, 0 is falsy. In Ruby, only two things are falsy: nil and false. Everything else — including 0, empty strings, and empty arrays — is truthy.
| Value | Ruby | TypeScript | Java |
|---|---|---|---|
false | Falsy | Falsy | Falsy |
nil/null | Falsy | Falsy | NPE |
0 | Truthy | Falsy | Falsy |
"" | Truthy | Falsy | N/A |
[] | Truthy | Truthy | N/A |
This means your TypeScript instincts will betray you:
count = 0
if count
puts "This WILL print in Ruby!" # 0 is truthy
end
# What you probably meant:
if count > 0
puts "Now this checks what you actually want"
end
The simpler rule is actually easier once you internalize it. Only nil and false are falsy. No ambiguity about 0 vs undefined vs "" vs null.
Symbols: The Thing TypeScript Doesn’t Have#
:name # a symbol — immutable, reusable identifier
"name" # a string — new object every time
{ name: "Amy" } # shorthand for { :name => "Amy" }
Symbols are like string constants that Ruby interns in memory. :name is the same object every time you reference it. "name" creates a new string each time (though Ruby optimizes frozen strings).
The closest TypeScript equivalent is a string literal type ("name") or an enum key. In practice, symbols are used for hash keys, method names, and anywhere you’d use a string that never changes.
Blocks: Arrow Functions, But Different#
Ruby blocks are closures you pass to methods. They look like arrow functions but have their own syntax:
# Ruby # TypeScript
[1, 2, 3].map { |n| n * 2 } # [1, 2, 3].map(n => n * 2)
[1, 2, 3].select { |n| n > 1 } # [1, 2, 3].filter(n => n > 1)
[1, 2, 3].each { |n| puts n } # [1, 2, 3].forEach(n => console.log(n))
5.times { |i| puts i } # for (let i = 0; i < 5; i++) console.log(i)
The |n| syntax is the parameter list (like n => in TS). Single-line blocks use { }, multi-line blocks use do...end. It’s a stylistic convention, not a rule.
nil Is an Object (Not a Landmine)#
In Java, calling a method on null throws NullPointerException. In TypeScript, accessing a property on null throws TypeError. In Ruby, nil is an actual object — an instance of NilClass — and you can call methods on it:
nil.to_s # "" (empty string)
nil.to_a # [] (empty array)
nil.to_i # 0
nil.nil? # true
nil.class # NilClass
Ruby has &. (safe navigation) just like TypeScript’s ?.:
user&.name&.upcase # Ruby: nil if user is nil
user?.name?.toUpperCase() // TypeScript: undefined if user is null/undefined
And Rails adds blank?, present?, and presence — which handle nil, empty strings, and whitespace in one check:
"".blank? # true
" ".blank? # true (whitespace only!)
nil.blank? # true
"hello".blank? # false
# The killer feature: presence
name = params[:name].presence || "Anonymous"
# Returns the value if present, nil if blank — then || provides the default
TypeScript has no built-in equivalent. You’d write a helper function or chain multiple checks.
No Imports (Wait, What?)#
This was the most disorienting thing coming from TypeScript. Ruby on Rails has no import or require statements in your application code. You just… use classes.
# In TypeScript, you'd write:
# import { Url } from '../models/url';
# import { UrlShortenerService } from '../services/url-shortener';
# In Rails, you just use them:
class UrlsController < ApplicationController
def create
@url = UrlShortenerService.call(url_params)
redirect_to @url
end
end
Rails uses a system called Zeitwerk that autoloads classes based on file path and naming conventions. UrlShortenerService maps to app/services/url_shortener_service.rb. Url maps to app/models/url.rb. The convention IS the import.
This felt like magic at first and then felt obvious. If the class name tells you the file location, why would you need to spell it out?
The Memoization Idiom#
@result ||= expensive_computation
This one-liner caches the result of expensive_computation in @result. If @result is nil (first call), it runs the computation and stores it. If @result already has a value (subsequent calls), it skips the computation.
TypeScript equivalent:
this.result ??= expensiveComputation();
Ruby uses ||= everywhere — lazy initialization, caching, default values. It’s one of the first idioms you’ll pick up because you see it in every codebase.
The Translation Cheat Sheet#
| Concept | TypeScript | Ruby |
|---|---|---|
| String interpolation | `Hello, ${name}` | "Hello, \#{name}" |
| Null/nothing | null, undefined | nil (just one) |
| Optional chaining | ?. | &. |
| Arrow function | (n) => n * 2 | { |n| n * 2 } |
console.log() | puts | |
| Throw | throw new Error() | raise "error" |
| Try/catch | try {} catch {} | begin...rescue...end |
| Import | import X from 'y' | Nothing (Zeitwerk autoloads) |
| Default param | function f(x = 5) | def f(x = 5) |
| Ternary | a ? b : c | a ? b : c (same!) |
Some things are the same. Some things are simpler. And some things — like truthiness — will trip you up until you rewire your instincts. The next post covers the framework side: Rails conventions, MVC, and ActiveRecord.
