T-Ruby: Adding Static Typing to Ruby Without Runtime Overhead
Source: Dev.to

Introduction
Static typing is a formidable tool that brings immense value to codebases of all sizes. From tiny scripts to massive monoliths, the benefits are hard to ignore: you get live documentation that is always up to date, enhanced readability, and a reliable safety net that significantly boosts code reliability.
Different dynamic languages have taken various paths to solve the typing puzzle. Python introduced native but optional type hints, while the JavaScript ecosystem moved toward a separate language entirely with TypeScript. Within the Ruby community, we’ve seen two distinct implementations in RBS and Sorbet, which have recently begun to converge thanks to the introduction of RBS inline and Sorbet’s support for it.
However, the current Ruby approach isn’t without friction. For many developers, typing still feels like a matter of personal preference rather than a core requirement. Because type checking is often seen as “extra,” it is far too easy for static checks to be ignored or forgotten entirely.
What Is T‑Ruby?
A groundbreaking experiment gaining traction is T‑Ruby. Unlike standard runtime type‑checking systems, T‑Ruby compiles typed code down to pure, undecorated Ruby with zero runtime overhead. In short, T‑Ruby is essentially TypeScript for the Ruby language: you write RBS‑inspired code that is eventually compiled into plain, standard Ruby.
While some might suggest using Crystal, there is a fundamental difference in philosophy. Crystal is a separate, Ruby‑like language with its own ecosystem. T‑Ruby, by contrast, is an extra layer designed to stay close to the Ruby we already know. Much like how TypeScript eventually outputs plain JavaScript, T‑Ruby ensures your final product remains pure Ruby.
Example: Typed HTTP Client
Below is a simple HTTP client that fetches the latest Ruby release tag from GitHub. With T‑Ruby, types are embedded directly into the class structure.
require "httparty"
require "json"
require "time"
class RubyVersion
API_URL = "https://api.github.com/repos/ruby/ruby/releases/latest"
class Response
attr_reader :code: Integer
attr_reader :json: Hash
def initialize(code: Integer, json: Hash): nil
@code = code
@json = json
end
def success?: Boolean
(200..299).include?(@code)
end
end
def fetch_response: Response
http = HTTParty.get(API_URL, headers: { 'User-Agent' => 'static-typing-demo' })
Response.new(http.code.to_i, JSON.parse(http.body))
end
def self.fetch(printer: Proc<[String, String, Time], nil]): nil
resp = new.fetch_response
raise "HTTP #{resp.code}" unless (200..299).include?(resp.code)
data = resp.json
published = Time.parse((data['published_at'] || Time.now.utc.iso8601).to_s)
printer.call((data['tag_name'] || data['name']).to_s, data['html_url'], published)
end
end
By adding these types, we can strictly define the structure of the Proc and ensure the data passed through the system is valid. In standard Ruby, we would normally rely on documentation or manual guard clauses to achieve this level of certainty.
Setting Up T‑Ruby
-
Install the gem:
gem install t-ruby -
Initialize a configuration file:
trc --init
The generated config lets you specify source folders for *.trb files, the destination for compiled Ruby, and post‑compile commands such as auto‑running RSpec or Minitest.
Rails Integration (Experimental)
Rails relies on a specific folder structure, so the cleanest approach is to store the entire app folder within your T‑Ruby source directory. Files that don’t contain T‑Ruby syntax are simply copied over to the destination folder unmodified. This mirrors the /src / /dist workflow common in modern JavaScript frameworks like Next.js.
Current Status and Outlook
T‑Ruby is still in a technical preview phase. The website and documentation look polished, but the implementation can be temperamental—official examples may fail immediately after installation.
Despite these rough edges, the potential is undeniable. Working on a small Rails app with T‑Ruby eliminated the need for a separate collection of RBS files. Writing types alongside business logic feels natural, and knowing the code is “type‑correct” at compile time provides a unique peace of mind. It’s a bold step forward for the Ruby ecosystem, even if it isn’t quite ready for production environments just yet.
Originally posted in my blog.