In his RubyConf 2021 keynote, the creator of Ruby, Yukihiro Matsumoto, announced TypeProf-IDE, a Visual Studio Code integration for Ruby’s TypeProf tool to allow real-time type analysis and developer feedback. In another session, the creator of TypeProf-IDE, Yusuke Endoh, demoed the extension in more detail. This functionality is available for us to try today in Ruby 3.1.0 preview 1, which was released during RubyConf. So let’s give it a try!
First, install Ruby 3.1.0 preview 1. If you’re using
rbenv on macOS, you can install the preview by executing the following commands in order:
brew upgrade ruby-build
rbenv install 3.1.0-preview1
Next, create a project folder:
If you’re using
rbenv, you can configure the preview to be the version of Ruby used in that directory:
rbenv local 3.1.0-preview1
Next, initialize the
typeprof_sandbox folder in VS Code. Next, open the
Gemfile and add the
git_source(:github) "https://github.com/#repo_name" #gem "rails" +gem 'typeprof', '0.20.3'
Now install it:
Getting Type Feedback
To see TypeProf in action, let’s create a class for keeping track of members of a meetup group. Create a file
meetup.rb and add the following:
class Meetup def initialize @members =  end def add_member(member) @members.push(member) end def first_member @members.first end end
It’s possible you will already see TypeProf add type signatures to the file, but more likely you won’t see anything yet. If not, to find out what’s going on, click the “View” menu, then choose “Output”. From the dropdown at the right, choose “Ruby TypeProf”. You’ll see the output of the TypeProf extension, which will likely include a Ruby-related error. What I see is:
[vscode] Try to start TypeProf for IDE [vscode] stderr: --- ERROR REPORT TEMPLATE ------------------------------------------------------- [vscode] stderr: [vscode] stderr: ``` [vscode] stderr: Gem::GemNotFoundException: can't find gem bundler (= 2.2.31) with executable bundle [vscode] stderr: /Library/Ruby/Site/2.6.0/rubygems.rb:278:in `find_spec_for_exe'
In my case, the command is running in my macOS system Ruby (
/Library/Ruby/Site/2.6.0) instead of my
rbenv-specified version. I haven’t been able to figure out how to get it to use the
rbenv version. As a workaround, I switched to the system Ruby and updated Bundler:
rbenv local system
sudo gem update bundler
rbenv local 3.1.0-preview1
For more help getting the extension running, check the TypeProf-IDE Troubleshooting docs. Of note is the different places that the extension tries to invoke
typeprof from. Ensure that your default shell is loading Ruby
3.1.0-preview1 and that a
typeprof binary is available wherever the extension is looking.
After addressing whatever error you see in the output, quit and reopen VS Code to get the extension to reload. When it succeeds, you should see output like the following:
[vscode] Try to start TypeProf for IDE [vscode] typeprof version: typeprof 0.20.2 [vscode] Starting Ruby TypeProf (typeprof 0.20.2)... [vscode] Ruby TypeProf is running [Info - 9:03:49 AM] TypeProf for IDE is started successfully
You should also see some type information added above the methods of the class:
Well, that’s not a lot of information. We see that
#add_member takes in an argument named
member, but its type is listed as
untyped (which means, the type information is unknown). It returns an
Array[untyped], meaning an array containing elements whose type is unknown. Then
#first_member says it returns
nil, which is incorrect.
Improving the Types and Code
For our first change, let’s look at the return value of
#add_member. It’s returning an
Array, but I didn’t intend to return a value; this is just a result of Ruby automatically returning the value of the last expression in a method. Let’s update our code to remove this unintentional behavior. Add a
nil as the last expression of the method:
def add_member(member) @members.push(member) + nil end
Now the return type is updated to be
NilClass, which is better:
Next, how can we fix the
untyped? Endoh recommends a pattern of adding some example code to the file showing the use of the class. Add the following at the bottom of
if $PROGRAM_NAME == __FILE__ meetup = Meetup.new end
meetup.ad below the line where
meetup is assigned. (We’ll explain the
$PROGRAM_NAME line in a bit.) An autocomplete dropdown will appear, with
Because TypeProf can see that
meetup is an instance of class
Meetup, it can provide autocomplete suggestions for methods.
add_member in the list, then type an opening parenthesis
(. VS Code will add the closing parenthesis
) after the cursor, and another popup will appear with information about the method’s arguments:
It indicates that the method takes one argument,
member, and returns
nil. Also note that the type of
member is still listed as
untyped; we’re still working toward fixing that.
Pass a string containing your name as the argument, then add the rest of the code below:
if $PROGRAM_NAME == __FILE__ meetup = Meetup.new meetup.add_member('Josh') first_member = meetup.first_member puts first_member end
What’s the significance of the
if $PROGRAM_NAME == __FILE__ conditional?
$PROGRAM_NAME is the name of the currently running program, and
__FILE__ is the name of the current source file. If they are equal, that means that this Ruby file is being executed directly, which includes when TypeProf runs the file. So this is a way to provide supplemental information to TypeProf.
When you added this code, the type information should have updated to:
Why does this added code affect the type of information? TypeProf executes the code to see the types that are actually used by the program. By supplying an example of using the class, TypeProf has more type information to work with. Future TypeProf development may allow it to be more intelligent about inferring type information from RSpec tests and usages elsewhere in the project.
Note that TypeProf now indicates that the
member argument is a
String, and that
#first_member may return either a
NilClass or a
String. (The reason it might return a
NilClass is if the array is empty.)
Making the Code Generic with Type Variables
Let’s put our object-oriented design hats on and think about these types. Is it specific to
Strings? No, the code doesn’t make any assumptions about what the members are. But TypeProf has coupled our class to one specific other class to use!
To prevent this, we can manually edit the RBS type signatures generated for our class to indicate just how generic we want
Meetup to be.
Create an empty
typeprof.rbs file in your project folder. Next, command-click on the type signature above
typeprof.rbs file will open, and the RBS type signature for that method will be automatically added to it:
Next, go back to
meetup.rb and right-click the type signature above
#first_member. This adds the signature for that method to the RBS file too, but as a separate
To keep things simpler, edit the RBS file so there’s a single
class with two methods in the same order as in the Ruby file, and save the file:
Now, let’s edit the signature to use type variables. A type variable is a place where, instead of referencing a specific type, you use a variable that can represent any type. Everywhere the same type variable appears, the type must be the same.
First, add a
[MemberT] after the
Meetup class name:
Next, replace the two occurrences of
What this means is, a given
Meetup instance applies to a certain type, called
MemberT. That’s the type of the
member you pass in to
#add_member. That is the same type as what the return value of
#first_member should be. So if you pass in a
String you should get a
String back. If you pass in a
Hash, you should get a
Switch back to
meetup.rb. If you don’t see the type signatures updated, you may need to close and reopen
meetup.rb. Then, you should see updated type signatures:
Note that our
MemberT types appear in the signatures of
#first_member. Also note that the signatures have a
# in front of them: this indicates that they’re manually specified in the RBS file.
Now, let’s see what help this gives us. In the statement
puts first_member, start typing
.up after it. Note that an autocomplete dropdown appears and
#upcase is selected:
TypeProf knows that
member is a
Meetup object. Because you passed a
String into the
#add_member method of the
meetup object, TypeProf can tell that
meetup’s type variable
MemberT is equal to the type
String. As a result, it can see that its
#first_member method will also return a
String. So it knows
first_member is a string, and therefore it can suggest
String’s methods for the autocomplete.
upcase to autocomplete it. Now note that
first_member.upcase has a red squiggly underlining it. Hover over it to see the error:
[error] undefined method: nil#upcase. But wait, isn’t
String? The answer is maybe. But it could also be a
nil if the meetup hasn’t had any members added to it. And if it is
nil, this call to
#upcase will throw a
NoMethodError. Now, in this trivial program we know there will be a member present. But for a larger program, TypeProf will have alerted us to an unhandled edge case!
To fix this, we need to change the way the type signature is written slightly. In the RBS file, replace
(NilClass | MemberT) with
MemberT? (don’t miss the question mark):
? indicates an optional type, a case where a value could be a certain type or it could be
Now, in the Ruby file, wrap the
puts call in a conditional:
first_member = meetup.first_member -puts first_member.upcase +if first_member + puts first_member.upcase +else + puts 'first_member is nil' +end
If the red squiggly under the call to
#upcase doesn’t disappear, close and reopen
meetup.rb to get TypeProf to rerun. After that, if you made the changes correctly, the underline should disappear:
TypeProf has guided us to write more robust code! Note that currently TypeProf requires the check to be written as
if variable; other idioms like
unless variable.nil? and
if variable.present? will not yet work.
If you’d like to learn more about TypeProf-IDE, Endoh’s RubyConf 2021 talk should be uploaded to YouTube within a few months. In the meantime, check out the TypeProf-IDE documentation and the RBS syntax docs. And you can help with the continued development of TypeProf-IDE by opening a GitHub Issue on the
Thank you to Yusuke Endoh for his hard work building the TypeProf-IDE integration, for his presentation, and for helping me work through issues using it during RubyConf!
If you’d like to work at a place that explores the cutting edge of Ruby and other languages, join us at Big Nerd Ranch!