Ruby is a dynamically typed language that favors duck typing over strict type enforcement. However, there are cases where type checking can be useful to avoid unexpected behavior. In this post, we’ll explore various ways to perform type validation and type checking in Ruby.
Type Checking and Type Casting in Ruby
Yes, even though Ruby does not enforce types at the language level, there are several techniques to validate the types of method parameters. Below are some approaches:
1. Manual Type Checking with raise
One straightforward way to enforce type checks is by manually verifying the type of a parameter using is_a? and raising an error if it does not match the expected type.
def my_method(arg)
raise TypeError, "Expected String, got #{arg.class}" unless arg.is_a?(String)
puts "Valid input: #{arg}"
end
my_method("Hello") # Works fine
my_method(123) # Raises: TypeError: Expected String, got Integer
2. Using respond_to? for Duck Typing
Rather than enforcing a strict class type, we can check whether an object responds to a specific method.
def my_method(arg)
unless arg.respond_to?(:to_str)
raise TypeError, "Expected a string-like object, got #{arg.class}"
end
puts "Valid input: #{arg}"
end
my_method("Hello") # Works fine
my_method(:symbol) # Raises TypeError
3. Using Ruby 3’s Type Signatures (RBS)
Ruby 3 introduced RBS and TypeProf for static type checking. You can define types in an .rbs file:
def my_method: (String) -> void
Then, you can use tools like steep, a static type checker for Ruby, to enforce type checking at development time.
How to Use Steep for Type Checking
Steep does not use annotations or perform type inference on its own. Instead, it relies on .rbi files to define type signatures. Here’s how you can use Steep for type checking:
- Define a Ruby Class:
class Calculator
def initialize(value)
@value = value
end
def double
@value * 2
end
end
- Generate an
.rbiFile:
steep scaffold calculator.rb > sig/calculator.rbi
This generates an .rbi file, but initially, it will use any for all types. You need to manually edit it to specify proper types.
- Modify the
.rbiFile to Define Types:
class Calculator
@value: Integer
def initialize: (Integer) -> void
def double: () -> Integer
end
- Run Steep to Check Types:
steep check
Steep also supports generics and union types, making it a powerful but less intrusive type-checking tool compared to Sorbet.
4. Using Sorbet for Stronger Type Checking
Sorbet is a third-party static type checker that allows you to enforce type constraints at runtime.
require 'sorbet-runtime'
extend T::Sig
sig { params(arg: String).void }
def my_method(arg)
puts "Valid input: #{arg}"
end
my_method("Hello") # Works fine
my_method(123) # Raises error at runtime
References:
Another Approach: Using Rescue for Type Validation
A different way to handle type checking is by using exception handling (rescue) to catch unexpected types and enforce validation.
def process_order(order_items, customer_name, discount_code)
# Main logic
...
rescue => e
# Type and validation checks
raise "Expecting an array of items: #{order_items.inspect}" unless order_items.is_a?(Array)
raise "Order must contain at least one item: #{order_items.inspect}" if order_items.empty?
raise "Expecting a string for customer name: #{customer_name.inspect}" unless customer_name.is_a?(String)
raise "Customer name cannot be empty" if customer_name.strip.empty?
raise "Unexpected error in `process_order`: #{e.message}"
end
Summary
- Use
is_a?orrespond_to?for runtime type checking. - Use Ruby 3’s RBS for static type enforcement.
- Use Sorbet for stricter type checking at runtime.
- Use Steep for static type checking with RBS.
- Exception handling can be used for validating types dynamically.
Additional Considerations
Ruby is a dynamically typed language, and unit tests can often be more effective than type checks in ensuring correctness. Writing tests ensures that method contracts are upheld for expected data.
For Ruby versions prior to 3.0, install the rbs gem separately to define types for classes.
If a method is defined, it will likely be called. If reasonable tests exist, every method will be executed and checked. Therefore, instead of adding excessive type checks, investing time in writing tests can be a better strategy.