Skip to main content
  1. Posts/

Everything Is an Object

·1061 words·5 mins
Photograph By Thought Catalog
Blog Ruby Ruby on Rails
Table of Contents
Rails for the TypeScript Engineer - This article is part of a series.
Part 1: This Article

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.

ValueRubyTypeScriptJava
falseFalsyFalsyFalsy
nil/nullFalsyFalsyNPE
0TruthyFalsyFalsy
""TruthyFalsyN/A
[]TruthyTruthyN/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
#

ConceptTypeScriptRuby
String interpolation`Hello, ${name}`"Hello, \#{name}"
Null/nothingnull, undefinednil (just one)
Optional chaining?.&.
Arrow function(n) => n * 2{ |n| n * 2 }
Printconsole.log()puts
Throwthrow new Error()raise "error"
Try/catchtry {} catch {}begin...rescue...end
Importimport X from 'y'Nothing (Zeitwerk autoloads)
Default paramfunction f(x = 5)def f(x = 5)
Ternarya ? b : ca ? 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.

Aaron Yong
Author
Aaron Yong
Building things for the web. Writing about development, Linux, cloud, and everything in between.
Rails for the TypeScript Engineer - This article is part of a series.
Part 1: This Article

Related

The Data Structure That's Okay With Being Wrong
·1340 words·7 mins
Photograph By Elimende Inagella
Blog Software Engineering Data Structures
Bloom filters — probabilistic, memory-efficient, and surprisingly useful
The Toolbox Nobody Opens
·1271 words·6 mins
Photograph By Alexander Schimmeck
Blog Software Engineering Computer Science
Data structures you use every day without thinking about them, and the ones you should
Sharing Is Caring
·1948 words·10 mins
Photograph By Elaine Casap
Blog JavaScript Web Development
Internal packages in monorepos — shared types, UI components, and the end of copy-paste engineering