I maintain the pdf-reader ruby gem and I'm using it to experiment with sorbet. I have no prior experience with sorbet.
I'd like to use types to improve the development experience, and distribute the type info with the gem so downstream users who use sorbet can benefit. However, I'd like to avoid adding a runtime sorbet dependency. Most downstream users do not use sorbet, and they shouldn't gain a new runtime dependency.
I think that means I should distribute the type info as a *.rbi file(s) inside the top level rbi/
directory. I'm not able to inline the types into my source (extend T::Sig
, etc).
During development (and test/ci) the type info in rbi/*.rbi
is useful for static type checking. However I can't rely on the types being correct at runtime (where downstream users might pass different types), so in some cases I still want to confirm the type like this:
def initialize(runs, mediabox)
raise ArgumentError, "a mediabox must be provided" if mediabox.nil?
...even though my rbi file declares mediabox can never be nil:
sig { params(runs: T::Array[PDF::Reader::TextRun], mediabox: T::Array[Numeric]).void }
def initialize(runs, mediabox); end
.. but then sorbet is unhappy with the code:
$ srb tc
./lib/pdf/reader/page_layout.rb:20: This code is unreachable https://srb.help/7006
20 | raise ArgumentError, "a mediabox must be provided" if mediabox.nil?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
./lib/pdf/reader/page_layout.rb:20: This condition was always falsy (T::Boolean)
20 | raise ArgumentError, "a mediabox must be provided" if mediabox.nil?
^^^^^^^^^^^^^
Got T::Boolean originating from:
./lib/pdf/reader/page_layout.rb:20:
20 | raise ArgumentError, "a mediabox must be provided" if mediabox.nil?
^^^^^^^^^^^^^
Errors: 1
I can explicitly ignore that error:
$ srb tc --suppress-error-code 7006
No errors! Great job.
Is there any way to keep the runtime type check without sorbet complaining, and without ignoring the error? Or maybe the "sorbet way" is just to remove the runtime check and live without it at runtime?
Or maybe my assumptions about only using rbi files for the type info are wrong?
Add a helper validation method:
sig { params(obj: Object, cls: Module).void }
def self.validate(obj, cls)
raise ArgumentError, "#{obj} must be a #{cls}" unless obj.is_a?(cls)
end
Now you can validate types throughout your code without adding a sorbet-runtime dependency.
You can of course also introduce variations such as validate_not_null
(for your specific example above). It does get a bit tricky with sorbet-specific types such as T::Array
, depending on how rigorous you want your validations to be.