Here's example code:
# typed: true
class KeyGetter
sig {params(env_var_name: String).returns(KeyGetter)}
def self.from_env_var(env_var_name)
return Null.new if env_var_name.nil?
return new(env_var_name)
end
def initialize(env_var_name)
@env_var_name = env_var_name
end
def to_key
"key from #{@env_var_name}"
end
def to_s
"str from #{@env_var_name}"
end
class Null
def to_key; end
def to_s; end
end
end
Running srb tc
on it fails with
key_getter.rb:7: Returning value that does not conform to method result type https://srb.help/7005
7 | return Null.new if env_var_name.nil?
^^^^^^^^^^^^^^^
Expected KeyGetter
key_getter.rb:6: Method from_env_var has return type KeyGetter
6 | def self.from_env_var(env_var_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Got KeyGetter::Null originating from:
key_getter.rb:7:
7 | return Null.new if env_var_name.nil?
^^^^^^^^
I see several ways of working around this:
- Use something like
.returns(T.any(KeyGetter, KeyGetter::Null))
in the sig. - Make
KeyGetter::Null
inherit fromKeyGetter
. Extract an "interface" and expect that.
class KeyGetter module Interface def to_key; end def to_s; end end class Null include KeyGetter::Interface end include Interface sig {params(env_var_name: String).returns(KeyGetter::Interface)} def self.from_env_var(env_var_name) return Null.new if env_var_name.nil? return new(env_var_name) end
But what I'd like to know (and didn't find in the docs) is: can I describe the duck type? Like we can do in YARD, for example:
# @returns [(#to_s, #to_key)]
Or is it an inherently flawed idea (because ideally we need to annotate the duck type's methods too. And not get lost in syntax while doing that).
So yes, can we annotate the duck type inline here? If not, what should we do instead?
KeyGetter#to_key
andKeyGetter#to_s
asreturn unless @env_var_name
thenNull
class is not technically needed. However I think that inheritance or abstraction is your best bet because I assume that the return value may be passed on to other method calls where the type is specified as well thusKeyGetter
andNull
would need to be considered the same type. – HormuzNull
here. It's only an illustration, though. Imagine another case: a method accepts a#call
(which returns, say, aUser
). So in production we pass a query object or something and in tests we can pass aProc
. Very useful. – Cawleysorbet
looks like a cool idea because you can leverage both but I don't see a way to provide a signature for that (would be a cool addition but definitely not static typed anymore). The other issue I see with static typing ruby is that you cannot overload a method, which is how other languages handle this concern. – Hormuzdefine_method
, but which also allows to specifiy for each parameter a set of methods it should respond to. – Appleton