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?