Just wanted to add. The answer above is correct. However, you should not expect behavior similar to TypeScript.
Unless I'm missing something evident (please correct me if I'm wrong), Steep will have a hard time "guessing" the exact shape of a hash.
Consider the following examples:
# class.rb: Illustrates usage of a simple class as a DTO
module Examples
module Dtos
module Class
class User
attr_reader :id, :login, :roles
def initialize(id:, login:, roles:)
@id = id
@login = login
@roles = roles
end
end
def self.run_example
user = User.new(id: 1, login: "john.doe", roles: ["admin"])
print_user(user)
end
def self.print_user(u)
puts "User: id=#{u.id} login=#{u.login} roles=#{u.roles}"
end
end
end
end
# class.rbs
module Examples
module Dtos
module Class
class User
attr_reader id: Integer
attr_reader login: String
attr_reader roles: Array[String]
def initialize: (id: Integer, login: String, roles: Array[String]) -> void
end
def self.run_example: () -> void
def self.print_user: (User u) -> void
end
end
end
# hash.rb: Illustrates usage of a Ruby Hash as a DTO
module Examples
module Dtos
module Hash
def self.run_example
user = { id: 2, login: "jenny.doe", roles: ["super_user"] }
print_user(user)
end
def self.print_user(u)
puts "User: id=#{u[:id]} login=#{u[:login]} roles=#{u[:roles]}"
end
end
end
end
#hash.rbs
module Examples
module Dtos
module Hash
type user_dto = {
id: Integer,
login: String,
roles: Array[String],
}
def self.run_example: () -> void
def self.print_user: (user_dto u) -> void
end
end
end
And I also have main.rb
where I run both examples:
# main.rb: Entry point
require_relative 'examples/dtos/class'
require_relative 'examples/dtos/hash'
def main
Examples::Dtos::Class.run_example
Examples::Dtos::Hash.run_example
end
main
So, in the hash.rb
example, Steep&RBS treats the user
hash as Hash<Symbol, Integer | String | Array<String>>
type, which means it is not able to infer the exact structure of a hash. Therefore, hash typing will not always give the desired effect. When I run the steep check
command for both examples, the one that uses class as a DTO works perfectly fine. However, the file that utilizes a Hash fails the type check with the following error message:
src/examples/dtos/hash.rb:7:19: [error] Cannot pass a value of type `::Hash[::Symbol, (::Integer | ::String | ::Array[::String])]` as an argument of type `::Examples::Dtos::Hash::user_dto`
│ ::Hash[::Symbol, (::Integer | ::String | ::Array[::String])] <: ::Examples::Dtos::Hash::user_dto
│ ::Hash[::Symbol, (::Integer | ::String | ::Array[::String])] <: { :id => ::Integer, :login => ::String, :roles => ::Array[::String] }
│
│ Diagnostic ID: Ruby::ArgumentTypeMismatch
│
└ print_user(user)
Conclusion:
If you're just starting a new project and you would like to use Steep to take advantage of type-checking in Ruby, you have to strongly consider utilizing simple classes as DTOs in your application.