Type checking in Ruby

Has anyone tried type checking in Ruby using Sorbet? Or another tool?

We are using Typescript more and more for our JS code, and now I’m starting to miss code-time or compile-time type checking when coding in Ruby.

Finally :grin:

I like Typescript’s approach as it’s quite flexible, you still can set the any type, which is very handy when prototyping.

For me honestly it is not type checking that is the most interesting, but typo checking. With good SOLID (https://en.m.wikipedia.org/wiki/SOLID) design principles it is most of the time pretty clear what an incoming object does and has (what it is shouldn’t even care for SOLID purists). But, only seeing a typo during runtime, in a not that often called method… that’s what frustrates me.

Ruby uses object references that can point at anything (of any class) at any time, and be reassigned to point at anything else (of any class) whenever it is necessary. So type checking isn’t all that useful.

Yes, I know that often we get nil returned and assigned as the target of a reference and this needs to be checked before the code continues and assumes what the object referenced is. So for this we have Object#nil? and Object#is_a? methods.

But using them within methods to test argument types can be clunky. I think that the C-side of Ruby has a common function to test method arguments against an array of class identifiers. (Named something like "rb_check_arg" perhaps? ADD: I may be thinking of "rb_scan_args" which does something else simliar to #sprintf.) It would be nice if there was a similar pure Ruby method that worked the same.

Something like …

# Checks each argument in the args array against an array of class types or
# a single class type.
# @param args [Array] An array of the passed arguments to check.
# @param types [Array(Class),Class] Either an array of class identifiers to
# apply against each argument in turn, or a single class identifier to check
# against all arguments. (If an array is used it must have the same number
# of members as the argument array.)
# @raise [TypeError] Raises a TypeError exception with 2 calls removed from
# call stack so that the callstack indicates where the erroneous argument
# type originated in the code.
def check_args(args,types)
  if types.is_a?(Array)
    fail(ArgumentError,"Size mismatch for args and types arrays.",caller) if
      types.size != args.size
  end
  args.each_with_index do |arg,i|
    checktype = types.is_a?(Array) ? types[i] : types
    unless arg.is_a?(checktype)
      fail(
        TypeError,
        "#{checktype.name} for argument(#{i+1}) expected, got #{arg.class.name}.",
        caller(2)
      )
    end
  end
  true # No TypeError exception was raised.
end

NOTE: Something like this is normally only used for a public API. For internal APIs you often do not wish the code to be slowed down so much by all this type checking each time one of your methods get called. So you’d only use this during devlopement and remove any type checking calls for production releases.

Oh and it’s use would be like so …

def my_method(*args)
  check_args( args, [String,String,Numeric] )
  # The code for the method ...
end

… or …

def collect_data(firstname,lastname,age)
  check_args( [firstname,lastname,age], [String,String,Numeric] )
  # The code for the method ...
end