After imagining a typed CoffeeScript, I realized we need to go deeper. CoffeeScript was inspired by Ruby, but what about Ruby itself? Ruby has always been beautifully expressive, but it’s also been dynamically typed from day one. And while Sorbet and RBS have tried to add types, they feel bolted on. Awkward. Not quite Ruby.
What if Ruby had been designed with types from the beginning? Not as an afterthought, not as a separate file you maintain, but as a natural, optional part of the language itself? Let’s explore what that could look like.
The Problem with Sorbet and RBS
Before we reimagine Ruby with types, let’s acknowledge why the current solutions haven’t caught on widely.
Sorbet requires you to add # typed: true comments and use a separate type checker. Types look like this:
# typed: true
extend T::Sig
sig { params(name: String, age: Integer).returns(String) }
def greet(name, age)
"Hello #{name}, you are #{age}"
end
Code language: PHP (php)
RBS requires separate .rbs files with type signatures:
# user.rbs
class User
attr_reader name: String
attr_reader age: Integer
def initialize: (name: String, age: Integer) -> void
def greet: () -> String
end
Code language: CSS (css)
Both solutions have the same fundamental problem: they don’t feel like Ruby. Sorbet’s sig blocks are verbose and repetitive. RBS splits your code across multiple files, breaking the single-file mental model that makes Ruby so pleasant.
What we need is something that feels native. Something Matz might have designed if static typing had been a priority in 1995.
Core Design Principles
Let’s establish what TypedRuby should be:
- Types are optional everywhere. You can gradually type your codebase.
- Types are inline. No separate files, no
sigblocks. - Types feel like Ruby. Natural syntax that matches Ruby’s philosophy.
- Duck typing coexists with static typing. You choose when to be strict.
- Generic types are first-class. Collections, custom classes, everything.
- The syntax is minimal. Ruby is beautiful; types shouldn’t ruin that.
Basic Type Annotations
In TypeScript, you use colons. In Sorbet, you use sig blocks. TypedRuby could use a more natural Ruby approach with the :: operator we already know:
# Current Ruby
name = "Ivan"
age = 30
# TypedRuby with inline types
name :: String = "Ivan"
age :: Integer = 30
# Or with type inference
name = "Ivan" # inferred as String
age = 30 # inferred as Integer
Code language: PHP (php)
The :: operator already means “scope resolution” in Ruby, but in this context (before assignment), it means “has type”. It’s familiar to Ruby developers and reads naturally.
Method Signatures
Current Sorbet approach:
extend T::Sig
sig { params(name: String, age: T.nilable(Integer)).returns(String) }
def greet(name, age = nil)
age ? "Hello #{name}, #{age}" : "Hello #{name}"
end
Code language: JavaScript (javascript)
TypedRuby approach:
def greet(name :: String, age :: Integer? = nil) :: String
age ? "Hello #{name}, #{age}" : "Hello #{name}"
end
Code language: JavaScript (javascript)
Or with Ruby 3’s endless method syntax:
def greet(name :: String, age :: Integer? = nil) :: String =
age ? "Hello #{name}, #{age}" : "Hello #{name}"
Code language: JavaScript (javascript)
Much cleaner. The types are right there with the parameters, and the return type is at the end where it reads naturally: “define greet with these parameters, returning a String.”
Classes and Attributes
Current approach with Sorbet:
class User
extend T::Sig
sig { returns(String) }
attr_reader :name
sig { returns(Integer) }
attr_reader :age
sig { params(name: String, age: Integer).void }
def initialize(name, age)
@name = name
@age = age
end
end
TypedRuby approach:
class User
attr_reader of String, :name
attr_reader of Integer, :age
def initialize(@name :: String, @age :: Integer)
end
def birthday :: void
@age += 1
end
def greet :: String
"I'm #{@name}, #{@age} years old"
end
end
Code language: CSS (css)
Even better, we could introduce parameter properties like TypeScript:
class User
def initialize(@name :: String, @age :: Integer, @email :: String)
# @name, @age, and @email are automatically instance variables
end
end
Code language: CSS (css)
Generics: The Ruby Way
This is where it gets interesting. Ruby already has a beautiful way of working with collections. TypedRuby needs to extend that naturally.
TypeScript uses angle brackets:
class Container<T> {
private value: T;
constructor(value: T) { this.value = value; }
}
Code language: JavaScript (javascript)
Sorbet uses square brackets:
class Container
extend T::Generic
T = type_member
sig { params(value: T).void }
def initialize(value)
@value = value
end
end
TypedRuby could use a more natural syntax with of:
class Container of T
def initialize(@value :: T)
end
def get :: T
@value
end
def map of U, &block :: (T) -> U :: Container of U
Container.new(yield @value)
end
end
# Usage
container = Container of String with.new("hello")
lengths = container.map { |s| s.length } # Container of Integer
For multiple type parameters:
class Pair of K, V
def initialize(@key :: K, @value :: V)
end
def map_value of U, &block :: (V) -> U :: Pair of K, U
Pair.new(@key, yield @value)
end
end
Code language: CSS (css)
Generic Methods
Methods can be generic too:
def identity of T, value :: T :: T
value
end
def find_first of T, items :: Array of T, &predicate :: (T) -> Boolean :: T?
items.find(&predicate)
end
# Usage
result = find_first([1, 2, 3, 4]) { |n| n > 2 } # Integer?
Code language: PHP (php)
Array and Hash Types
Ruby’s arrays and hashes need type support:
# Arrays
numbers :: Array of Integer = [1, 2, 3, 4, 5]
names :: Array of String = ["Alice", "Bob", "Charlie"]
# Or using shorthand
numbers :: [Integer] = [1, 2, 3, 4, 5]
names :: [String] = ["Alice", "Bob", "Charlie"]
# Hashes
user_ages :: Hash of String, Integer = {
"Alice" => 30,
"Bob" => 25
}
# Or using shorthand
user_ages :: {String => Integer} = {
"Alice" => 30,
"Bob" => 25
}
# Symbol keys (very common in Ruby)
config :: {Symbol => String} = {
host: "localhost",
port: "3000"
}
Code language: PHP (php)
Union Types
Ruby’s dynamic nature often uses union types implicitly. Let’s make it explicit:
# TypeScript: string | number
value :: String | Integer = "hello"
value = 42 # OK
# Method with union return type
def find_user(id :: Integer) :: User | nil
User.find_by(id: id)
end
# Multiple unions
status :: "pending" | "active" | "completed" = "pending"
Code language: PHP (php)
Nullable Types
Ruby uses nil everywhere. TypedRuby needs to handle this elegantly:
# The ? suffix means "or nil"
name :: String? = nil
name = "Ivan" # OK
# Methods that might return nil
def find_user(id :: Integer) :: User?
User.find_by(id: id)
end
# Safe navigation works with types
user :: User? = find_user(123)
email = user&.email # String? inferred
Code language: PHP (php)
Interfaces and Modules
Ruby uses modules for interfaces. TypedRuby could extend this:
interface Comparable of T
def <=>(other :: T) :: Integer
end
interface Enumerable of T
def each(&block :: (T) -> void) :: void
end
# Implementation
class User
include Comparable of User
attr_reader :name :: String
def initialize(@name :: String)
end
def <=>(other :: User) :: Integer
name <=> other.name
end
end
Code language: HTML, XML (xml)
Type Aliases
Creating reusable type definitions:
type UserId = Integer
type Email = String
type UserStatus = "active" | "inactive" | "banned"
type Result of T =
{ success: true, value: T } |
{ success: false, error: String }
def create_user(name :: String) :: Result of User
user = User.create(name: name)
if user.persisted?
{ success: true, value: user }
else
{ success: false, error: user.errors.full_messages.join(", ") }
end
end
Code language: JavaScript (javascript)
Practical Example: A Repository Pattern
Let’s build something real. Here’s a generic repository in TypedRuby:
interface Repository of T
def find(id :: Integer) :: T?
def all :: [T]
def create(attributes :: Hash) :: T
def update(id :: Integer, attributes :: Hash) :: T?
def delete(id :: Integer) :: Boolean
end
class ActiveRecordRepository of T implements Repository of T
def initialize(@model_class :: Class)
end
def find(id :: Integer) :: T?
@model_class.find_by(id: id)
end
def all :: [T]
@model_class.all.to_a
end
def create(attributes :: Hash) :: T
@model_class.create!(attributes)
end
def update(id :: Integer, attributes :: Hash) :: T?
record = find(id)
return nil unless record
record.update!(attributes)
record
end
def delete(id :: Integer) :: Boolean
record = find(id)
return false unless record
record.destroy!
true
end
end
# Usage
user_repo = ActiveRecordRepository of User .new(User)
users :: [User] = user_repo.all
user :: User? = user_repo.find(123)
Code language: CSS (css)
Blocks and Procs with Types
Blocks are fundamental to Ruby. They need proper type support:
# Block parameter types
def map of T, U, items :: [T], &block :: (T) -> U :: [U]
items.map(&block)
end
# Proc types
callback :: Proc of (String) -> void = ->(msg) { puts msg }
transformer :: Proc of (Integer) -> String = ->(n) { n.to_s }
# Lambda types
double :: Lambda of (Integer) -> Integer = ->(x) { x * 2 }
# Method that accepts a block with types
def with_timing of T, &block :: () -> T :: T
start_time = Time.now
result = yield
duration = Time.now - start_time
puts "Took #{duration} seconds"
result
end
# Usage
result :: String = with_timing { expensive_operation() }
Code language: PHP (php)
Rails Integration
Ruby is often Rails. TypedRuby needs to work beautifully with Rails. Here’s where we need to think carefully about syntax. For method calls that take parameters, we can use a generic-style syntax that feels natural.
Generic-style method calls for associations:
class User < ApplicationRecord
# Using 'of' with method calls (like generic instantiation)
has_many of Post, :posts
belongs_to of Company, :company
has_one of Profile?, :profile
# Or postfix style (reads more naturally)
has_many :posts of Post
belongs_to :company of Company
has_one :profile of Profile?
# For validations, types on the attribute names
validates :email of String, presence: true, uniqueness: true
validates :age of Integer, numericality: { greater_than: 0 }
# Scopes with return types
scope :active of Relation[User], -> { where(status: "active") }
scope :by_name of Relation[User], ->(name :: String) {
where("name LIKE ?", "%#{name}%")
}
# Typed callbacks still use :: for return types
before_save :normalize_email
def normalize_email :: void
self.email = email.downcase.strip
end
# Typed instance methods
def full_name :: String
"#{first_name} #{last_name}"
end
def posts_count :: Integer
posts.count
end
end
Code language: HTML, XML (xml)
Alternative: Square bracket syntax (like actual generics):
class User < ApplicationRecord
# Using square brackets like generic type parameters
has_many[Post] :posts
belongs_to[Company] :company
has_one[Profile?] :profile
# With additional options
has_many[Post] :posts, dependent: :destroy
has_many[Comment] :comments, through: :posts
# Validations
validates[String] :email, presence: true, uniqueness: true
validates[Integer] :age, numericality: { greater_than: 0 }
# Scopes
scope[Relation[User]] :active, -> { where(status: "active") }
scope[Relation[User]] :by_name, ->(name :: String) {
where("name LIKE ?", "%#{name}%")
}
end
Code language: HTML, XML (xml)
Comparison of syntaxes:
# Option 1: Postfix 'of' (most Ruby-like)
has_many :posts of Post
validates :email of String, presence: true
# Option 2: Prefix 'of' (generic-like)
has_many of Post, :posts
validates of String, :email, presence: true
# Option 3: Square brackets (actual generics)
has_many[Post] :posts
validates[String] :email, presence: true
# Option 4: 'as:' keyword (traditional keyword argument)
has_many :posts, as: [Post]
validates :email, as: String, presence: true
# Option 5: '<>' Angled brackets (traditional keyword argument)
has_many<[Post]> :posts
validates<String> :email, presence: true
Code language: PHP (php)
I personally prefer Option 2 (prefix ‘of’) because:
- It reads naturally in English: “has many of Post type”
- The symbol comes first (Ruby convention)
- It’s unambiguous and parser-friendly
- It feels like a natural Ruby extension
Full Rails example with postfix ‘of’:
class User < ApplicationRecord
has_many :posts of Post, dependent: :destroy
has_many :comments of Comment, through: :posts
belongs_to :company of Company
has_one :profile of Profile?
validates :email of String, presence: true, uniqueness: true
validates :age of Integer, numericality: { greater_than: 0 }
validates :status of "active" | "inactive" | "banned", inclusion: { in: %w[active inactive banned] }
scope :active of Relation[User], -> { where(status: "active") }
scope :by_company of Relation[User], ->(company_id :: Integer) {
where(company_id: company_id)
}
before_save :normalize_email
after_create :send_welcome_email
def normalize_email :: void
self.email = email.downcase.strip
end
def full_name :: String
"#{first_name} #{last_name}"
end
def recent_posts(limit :: Integer = 10) :: [Post]
posts.order(created_at: :desc).limit(limit).to_a
end
end
class PostsController < ApplicationController
def index :: void
@posts :: [Post] = Post.includes(:user).order(created_at: :desc)
end
def show :: void
@post :: Post = Post.find(params[:id])
end
def create :: void
@post :: Post = Post.new(post_params)
if @post.save
redirect_to @post, notice: "Post created"
else
render :new, status: :unprocessable_entity
end
end
private
def post_params :: Hash
params.require(:post).permit(:title, :body, :user_id)
end
end
Code language: HTML, XML (xml)
How it works under the hood:
The of keyword in method calls would be syntactic sugar that the parser recognizes:
# What you write:
has_many :posts of Post
# What the parser sees:
has_many(:posts, __type__: Post)
# Rails can then use this:
def has_many(name, **options)
type = options.delete(:__type__)
# Define the association
define_method(name) do
# ... normal association logic
end
# Store type information for runtime validation/documentation
if type
association_types[name] = type
# Optional runtime validation in development
if Rails.env.development?
define_method(name) do
result = super()
validate_type!(result, type)
result
end
end
end
end
Code language: PHP (php)
This approach:
- Keeps the symbol first (Ruby convention)
- Uses familiar
ofkeyword (like we use for generics) - Works with all existing parameters
- Is parser-friendly and unambiguous
- Reads naturally in English
Complex Example: A Service Object
Let’s build a realistic service object with full type safety:
type TransferResult =
{ success: true, transaction: Transaction } |
{ success: false, error: String }
class MoneyTransferService
def initialize(
@from_account :: Account,
@to_account :: Account,
@amount :: BigDecimal
)
end
def call :: TransferResult
return error("Amount must be positive") if @amount <= 0
return error("Insufficient funds") if @from_account.balance < @amount
return error("Accounts must be different") if @from_account == @to_account
transaction :: Transaction? = nil
Account.transaction do
@from_account.withdraw(@amount)
@to_account.deposit(@amount)
transaction = Transaction.create!(
from_account: @from_account,
to_account: @to_account,
amount: @amount,
status: "completed"
)
end
{ success: true, transaction: transaction }
rescue ActiveRecord::RecordInvalid => e
error(e.message)
end
private
def error(message :: String) :: TransferResult
{ success: false, error: message }
end
end
# Usage
service = MoneyTransferService.new(from_account, to_account, 100.50)
result :: TransferResult = service.call
case result
in { success: true, transaction: tx }
puts "Transfer successful: #{tx.id}"
in { success: false, error: err }
puts "Transfer failed: #{err}"
end
Pattern Matching with Types
Ruby 3 introduced pattern matching. TypedRuby makes it type-safe:
type Response of T =
{ status: "ok", data: T } |
{ status: "error", message: String } |
{ status: "loading" }
def handle_response of T, response :: Response of T :: String
case response
in { status: "ok", data: data :: T }
"Success: #{data}"
in { status: "error", message: msg :: String }
"Error: #{msg}"
in { status: "loading" }
"Loading..."
end
end
# Usage
user_response :: Response of User = fetch_user(123)
message = handle_response(user_response)
Code language: PHP (php)
Metaprogramming with Types
Ruby’s metaprogramming is powerful but dangerous. TypedRuby could make it safer:
class Model
def self.has_typed_attribute of T, name :: Symbol, type :: Class
define_method(name) :: T do
instance_variable_get("@#{name}")
end
define_method("#{name}=") :: void do |value :: T|
instance_variable_set("@#{name}", value)
end
end
end
class User < Model
has_typed_attribute of String, :name, String
has_typed_attribute of Integer, :age, Integer
end
user = User.new
user.name = "Ivan" # OK
user.age = 30 # OK
user.name = 123 # Type error!
Code language: HTML, XML (xml)
Gradual Typing
The beauty of TypedRuby is that it’s optional. You can mix typed and untyped code:
# Completely untyped (classic Ruby)
def process(data)
data.map { |x| x * 2 }
end
# Partially typed
def process(data :: Array)
data.map { |x| x * 2 }
end
# Fully typed
def process of T, data :: [T], &block :: (T) -> T :: [T]
data.map(&block)
end
# The three can coexist in the same codebase
Code language: PHP (php)
Type System and Object Hierarchy
Here’s a crucial question: how do types relate to Ruby’s object system? In Ruby, everything is an object, and every class inherits from Object (or BasicObject). TypedRuby’s type system needs to respect this.
Types ARE classes (mostly)
In TypedRuby, most types would literally be the classes themselves:
# String is both a class and a type
name :: String = "Ivan"
puts String.class # => Class
puts String.ancestors # => [String, Comparable, Object, Kernel, BasicObject]
# User is both a class and a type
user :: User = User.new
puts User.class # => Class
puts User.ancestors # => [User, ApplicationRecord, ActiveRecord::Base, Object, ...]
This is fundamentally different from TypeScript, where types exist only at compile time. In TypedRuby, types are runtime objects too.
Special type constructors
Some type syntax creates type objects at runtime:
# Array type constructor
posts :: [Post] = []
# This is roughly equivalent to:
posts :: Array[Post] = []
# Which could be implemented as:
class Array
def self.[](element_type)
TypedArray.new(element_type)
end
end
# Hash type constructor
ages :: {String => Integer} = {}
# Roughly:
ages :: Hash[String, Integer] = {}
The Type class hierarchy
TypedRuby would introduce a parallel type hierarchy:
# New base classes for type system
class Type
# Base class for all types
end
class GenericType < Type
# For parameterized types like Array[T], Hash[K,V]
attr_reader :type_params
def initialize(*type_params)
@type_params = type_params
end
end
class UnionType < Type
# For union types like String | Integer
attr_reader :types
def initialize(*types)
@types = types
end
end
class NullableType < Type
# For nullable types like String?
attr_reader :inner_type
def initialize(inner_type)
@inner_type = inner_type
end
end
# These would be used like:
array_of_posts = GenericType.new(Array, Post) # [Post]
string_or_int = UnionType.new(String, Integer) # String | Integer
nullable_user = NullableType.new(User) # User?
Code language: CSS (css)
Runtime type checking
Because types are objects, you could check them at runtime:
def process(value :: String | Integer)
case value
when String
value.upcase
when Integer
value * 2
end
end
# The type annotation creates a runtime check:
def process(value)
# Compiler inserts:
unless value.is_a?(String) || value.is_a?(Integer)
raise TypeError, "Expected String | Integer, got #{value.class}"
end
case value
when String
value.upcase
when Integer
value * 2
end
end
Code language: PHP (php)
Type as values (reflection)
Types being objects means you can work with them:
def type_info of T, value :: T :: Hash
{
value: value,
type: T,
class: value.class,
ancestors: T.ancestors
}
end
result = type_info("hello")
puts result[:type] # => String
puts result[:class] # => String
puts result[:ancestors] # => [String, Comparable, Object, ...]
# Generic types are objects too:
array_type = Array of String
puts array_type.class # => GenericType
puts array_type.type_params # => [String]
Method objects with type information
Ruby’s Method objects could expose type information:
class User
def greet(name :: String) :: String
"Hello, #{name}"
end
end
method = User.instance_method(:greet)
puts method.parameter_types # => [String]
puts method.return_type # => String
# This enables runtime validation:
def call_safely(obj, method_name, *args)
method = obj.method(method_name)
# Check argument types
method.parameter_types.each_with_index do |type, i|
unless args[i].is_a?(type)
raise TypeError, "Argument #{i} must be #{type}"
end
end
obj.send(method_name, *args)
end
Duck typing still works
Even with types, Ruby’s duck typing philosophy is preserved:
# You can still use duck typing without types
def quack(duck)
duck.quack
end
# Or enforce types when you want safety
def quack(duck :: Duck) :: String
duck.quack
end
# Or use interfaces for structural typing
interface Quackable
def quack :: String
end
def quack(duck :: Quackable) :: String
duck.quack # Works with any object that implements quack
end
Code language: CSS (css)
Type compatibility and inheritance
Types follow Ruby’s inheritance rules:
class Animal
def speak :: String
"Some sound"
end
end
class Dog < Animal
def speak :: String
"Woof"
end
end
# Dog is a subtype of Animal
def make_speak(animal :: Animal) :: String
animal.speak
end
dog = Dog.new
make_speak(dog) # OK, Dog < Animal
# Liskov Substitution Principle applies
animals :: [Animal] = [Dog.new, Cat.new, Bird.new]
The as: keyword and runtime behavior
When you write:
has_many :posts, as: [Post]
Code language: CSS (css)
This could be expanded by the Rails framework to:
has_many :posts, type_checker: -> (value) {
value.is_a?(Array) && value.all? { |item| item.is_a?(Post) }
}
Code language: JavaScript (javascript)
Rails could use this for runtime validation in development mode, giving you immediate feedback if you accidentally assign the wrong type.
Performance considerations
Runtime type checking has overhead. TypedRuby could handle this smartly:
# In development/test: full runtime checking
ENV['RUBY_TYPE_CHECKING'] = 'strict'
# In production: types checked only at compile time
ENV['RUBY_TYPE_CHECKING'] = 'none'
# Or selective checking for critical paths
ENV['RUBY_TYPE_CHECKING'] = 'public_apis'
Code language: PHP (php)
Integration with existing Ruby
Since types are objects, they integrate seamlessly:
# Works with reflection
User.instance_methods.each do |method|
m = User.instance_method(method)
if m.respond_to?(:return_type)
puts "#{method} returns #{m.return_type}"
end
end
# Works with metaprogramming
class User
[:name, :email, :age].each do |attr|
define_method(attr) :: String do
instance_variable_get("@#{attr}")
end
end
end
# Works with monkey patching (for better or worse)
class String
def original_upcase :: String
# Type information is preserved
end
end
This approach makes TypedRuby feel like a natural evolution of Ruby rather than a foreign type system bolted on. Types are just objects, following Ruby’s “everything is an object” philosophy.
TypedRuby should infer types aggressively:
# Inferred from literal
name = "Ivan" # String inferred
# Inferred from method return
def get_age
30
end
age = get_age # Integer inferred
# Inferred from array contents
numbers = [1, 2, 3, 4] # [Integer] inferred
# Inferred from hash
user = {
name: "Ivan",
age: 30,
active: true
} # {Symbol => String | Integer | Boolean} inferred
# Explicit typing when inference isn't enough
mixed :: [Integer | String] = [1, "two", 3]
Code language: PHP (php)
Why This Could Work
Unlike Sorbet and RBS, TypedRuby would be:
- Native: Types are part of the language syntax, not bolted on
- Optional: You choose where to add types
- Gradual: Mix typed and untyped code freely
- Readable: Syntax feels like Ruby, not like Java
- Powerful: Full generics, unions, intersections, pattern matching
- Practical: Works with Rails, metaprogramming, blocks, procs
The syntax respects Ruby’s philosophy. It’s minimal, expressive, and doesn’t get in your way. When you want types, they’re there. When you don’t, they’re not.
The Implementation Challenge
Could this be built? Technically, yes. You’d need to:
- Extend the Ruby parser to recognize type annotations
- Build a type checker that understands Ruby’s semantics
- Make it work with Ruby’s dynamic features
- Integrate with existing tools (RuboCop, RubyMine, VS Code)
- Handle the massive existing Ruby ecosystem
The hard part isn’t the syntax. It’s making the type checker smart enough to handle Ruby’s dynamism while still being useful. Ruby’s metaprogramming, method_missing, dynamic dispatch, these all make static typing hard.
But not impossible. Crystal proved you can have Ruby-like syntax with static types. Sorbet proved you can add types to Ruby code. TypedRuby would combine the best of both: native syntax with gradual typing.
The Dream
Imagine opening a Rails codebase and seeing:
class User < ApplicationRecord
has_many :posts :: [Post]
def full_name :: String
"#{first_name} #{last_name}"
end
end
class PostsController < ApplicationController
def create :: void
@post :: Post = Post.new(post_params)
@post.save!
redirect_to @post
end
end
The types are there when you need them, documenting the code and catching bugs. But they don’t dominate. The code still looks like Ruby. It still feels like Ruby.
That’s what TypedRuby could be. Not a separate type system bolted onto Ruby. Not a different language inspired by Ruby. But Ruby itself, evolved to support the type safety modern developers expect.
Would It Succeed?
Honestly? Probably not. Ruby’s community values dynamism and flexibility. Matz has explicitly said he doesn’t want mandatory typing. The ecosystem is built on duck typing and metaprogramming.
But that doesn’t mean it wouldn’t be useful. A significant portion of Ruby developers would adopt optional typing if it felt natural. Rails applications would benefit from type safety in controllers, models, and services. API clients would be more reliable. Refactoring would be safer.
The key is making it optional and making it Ruby. Not Sorbet’s verbose sig blocks. Not RBS’s separate files. Just Ruby, with types when you want them.
Conclusion
TypedRuby is a thought experiment, but it’s a valuable one. It shows what’s possible when you design types into a language from the start, rather than bolting them on later.
Ruby is beautiful. Types don’t have to ruin that beauty. With the right syntax, the right philosophy, and the right implementation, they could enhance it.
Maybe someday we’ll see Ruby 4.0 with native, optional type annotations. Maybe we won’t. But it’s fun to imagine a world where Ruby has the expressiveness we love and the type safety we need.
Until then, we have Sorbet and RBS. They’re not perfect, but they’re what we’ve got. And who knows? Maybe they’ll evolve. Maybe the syntax will improve. Maybe they’ll feel more Ruby-like over time.
Or maybe someone will read this and decide to build TypedRuby for real.
A developer can dream.