Why Ruby Doesn't Need Static Types: Embracing Its Dynamic Nature
Explore why introducing static types to Ruby contradicts its dynamic, expressive nature, leading to performance issues and code complexity, and advocating for its core principles.
You Don’t Need Types in Ruby
Published: Oct 29, 2025 | Read Time: 7 minutes
Written by Evgeny Zhdanov, Pragmatic software engineer.
Ruby was never intended to be statically typed. For decades, it has thrived as a dynamic, expressive, and human-oriented language. Yet, every few years, a new attempt emerges to reinvent it as a typed language. Let’s explore why this is a mistake and why Ruby should remain true to its essence.

A lively discussion on Hacker News here about Sorbet and static typing in Ruby inspired this post. It’s fascinating how people try to make Ruby emulate a statically typed language. However, this isn’t the first attempt, and each time, it ends up fighting against Ruby’s fundamental design principles.
Turning Ruby into Java isn’t progress; it’s a step backward. Let me explain why.
How Ruby Deals with Types
Ruby is a dynamically typed, object-oriented language designed around message passing — an idea borrowed from Smalltalk. In Ruby, we send messages to objects, not checkboxes of type signatures. This is what makes it flexible, expressive, and alive.
This concept is known as duck typing: if an object responds to a message, that’s all you need to know.
class CreditCard
def process(amount)
puts "Charging $#{amount} to credit card"
true
end
end
class BankTransfer
def process(amount)
puts "Transferring $#{amount} from bank account"
true
end
end
class Cash
def process(amount)
puts "Accepting $#{amount} cash"
true
end
end
def checkout(payment_method, amount)
payment_method.process(amount)
end
checkout(CreditCard.new, 50)
checkout(BankTransfer.new, 50)
checkout(Cash.new, 50)
That’s Ruby’s essence: you don’t care what it is, only what it can do. If a method takes an argument, you should be able to pass any object to it, as long as that object responds to the method being called.
Sandi Metz
Sandi Metz’s book, Practical Object-Oriented Design in Ruby, remains the best explanation of this philosophy. It’s not about enforcing constraints; it’s about designing objects that cooperate.
A Brief History of Type Experiments
Adding types to Ruby isn’t new. Many have tried; most have failed:
- RBS – Introduced with Ruby 3 as part of the “Ruby 3x3” initiative. It aimed to formalize type definitions but never gained significant real-world traction.
- dry-types – Part of the
dry-rbecosystem. It adds runtime type constraints but at the cost of slower performance. - typed-ruby, RTC, Rubype – Early academic or community efforts that never saw widespread adoption.
- Sorbet – The most popular attempt so far, backed by Stripe. It mixes static and runtime checks but at the expense of speed and simplicity/readability.
Ten years later, the discussion is still alive, but the core issue remains: Ruby was not designed for static typing.
Why Adding Types to Ruby Is a Bad Idea
Adding static types to a dynamic language is like putting a manual transmission in a Tesla – you’re bolting on complexity that contradicts the core architecture. It makes no sense. Here’s why:
-
Ruby doesn’t compile. Static type checks make sense in compiled languages. Ruby isn’t one; everything happens at runtime.
-
Runtime type checks hurt performance. Sorbet and similar systems add extra runtime overhead to simulate safety that compiled languages get for free. They often need to run checks at runtime to ensure type safety, which defeats the purpose of static typing.
-
Annotations pollute the codebase. Endless
sig { params(...) }blocks make code noisy without improving design.
sig { params(cat: Cat).void } def sound(cat) cat.sound end ```
This doesn’t make your program better; it just adds ceremony.
- It’s a design smell. If your Ruby code “needs” types to feel safe, that’s not a type problem; it’s a design problem. It probably needs refactoring, not annotations.
Ruby already provides implicit conversion hooks like to_int, to_str, to_ary, which are strict enough when you need type enforcement. You don’t need Sorbet for that.
So what’s the benefit of types here?
- Faster? No – slower.
- More readable? No – noisier.
- Safer? Barely.
It’s a bad trade-off. Matz himself said he doesn’t like types in Ruby, encouraging hiding types behind interfaces, not surfacing them as syntax 1. There are already excellent statically typed languages like Go, Java, Rust, and C++. Ruby isn’t one of them, and it shouldn’t pretend to be.
Runtime Performance Overhead
Even Sorbet’s own documentation admits it: “Enable Runtime Checks. Sorbet relies heavily on runtime type checks to back up its static predictions.”
These runtime checks aren’t free. Every method call now includes overhead: type extraction, signature validation, and error raising on mismatches. This happens in production, not just during development. The irony is profound: static typing exists to catch errors before runtime, yet Sorbet reintroduces runtime validation as a core feature.
Compare this to a linter. A linter runs during development, catches issues in your editor or CI pipeline, and ships nothing to production. Zero runtime cost. Sorbet, however, cannot be stripped out – there’s no “production mode” that removes the type checks. You’re paying the performance tax in the place that matters most: live traffic serving real users.
Why does a static type checker need to exist in production at all? It shouldn’t. The whole point of static analysis is to fail fast during development, not to babysit your code in production. Sorbet conflates compile-time safety with runtime validation, and you pay for it.
Maintainability
Imagine refactoring a large Ruby codebase littered with type annotations. Every method signature change cascades through the code, forcing updates to all related sig blocks. This creates a maintenance nightmare, especially in dynamic languages where flexibility is key.
The counter-argument might be that this is precisely what makes it type-safe. But in reality, it just adds friction to the development process. I believe this leads to maintainability hell for developers, who will avoid touching heavily annotated code. They might even sabotage the type system to get their work done faster.
Have you seen the reliance on the any type in the TypeScript world 2? Rules must be clear and simple to follow; otherwise, they will be violated. Improving such a codebase becomes harder, not easier. Refactoring becomes very time-consuming.
What to Do Instead
Instead of forcing static typing into Ruby, teach developers how to think in Ruby. If someone comes from Java, Go, or C#, help them unlearn the obsession with type systems. Show them:
-
Duck typing – It’s powerful, flexible, and elegant when used correctly.
-
Implicit conversion methods like
to_int,to_str, etc. Use them to enforce strictness when needed, though most of the time, you won’t need them. -
YARD – For documenting code with optional type hints. You can even hook it into LSP for autocompletion and hints.
@param [Animal] animal
@return [String]
def sound(animal) animal.sound end ```
YARD gives you clarity without runtime overhead.
- Tests – Still the best “type system” Ruby ever had. In Ruby, you have RSpec/Minitest to cover your codebase. Tests catch errors, verify expectations, and allow safe refactoring. They’re the true safety net of Ruby, not magic type annotations.
- Linters – Like RuboCop, to enforce style and catch common mistakes without any effect on runtime performance. Linters can catch performance issues, style violations, and common bugs.
The Cultural Problem
Modern engineering culture has developed a strange addiction to tools. We build abstractions for the sake of it, and then convince others it’s “best practice.” The more code I see, the more it feels like an adult kindergarten – full of toys and shiny tools, but not much craftsmanship.
Typing systems in Ruby fall right into that category. They give an illusion of safety while draining time, performance, and clarity. We care deeply about sustainability and climate change, but we think it’s fine to burn CPU cycles running pointless runtime type checks? Iron.io showed how performance matters – and waste adds up. The same logic applies here.
Conclusion
I’m strongly against forcing types into dynamic languages. It’s not just a philosophical issue; it’s a practical one. You lose performance, clarity, and the spirit of Ruby. There are many languages designed for static typing. Ruby isn’t one of them, and it shouldn’t pretend to be.
Footnotes
About the Author
Evgeny Zhdanov
Pragmatic software engineer
Subscribe to the newsletter for monthly emails summarizing new posts and insights on software engineering, developer life, and programmer philosophy. By subscribing, you agree to our Privacy Policy.