# Getting Started with TypeProf

[TypeProf](https://github.com/ruby/typeprof) is Ruby's official type inference engine that automatically generates RBS type signatures by analyzing your code. Developed by the Ruby core team and shipping with Ruby 3.0+, TypeProf eliminates the tedious work of writing type signatures by hand, letting you bootstrap type coverage for entire codebases in minutes.

What makes TypeProf compelling is its ability to understand Ruby's dynamic nature while producing static type information. Unlike tools that require you to annotate every method, TypeProf examines how values flow through your program and infers types automatically. You write normal Ruby code, run TypeProf, and get RBS signatures that document your interfaces.

This guide shows you how to use TypeProf effectively, understand what it can and can't infer, and leverage it to build comprehensive type coverage for your Ruby projects.

[ad-logs]

## Prerequisites

You'll need Ruby 3.0 or later since TypeProf ships as part of the standard library starting from that version. This guide assumes you're comfortable with Ruby fundamentals and have a basic understanding of types. Familiarity with RBS syntax helps but isn't required - TypeProf generates the signatures for you.

## Setting up your first TypeProf project

Let's create a Ruby project to explore how TypeProf analyzes code and generates signatures. Start by setting up a basic structure:

```command
mkdir typeprof-demo && cd typeprof-demo
```

```command
bundle init
```

Open the generated `Gemfile` and add TypeProf:

```ruby
[label Gemfile]
source 'https://rubygems.org'

[highlight]
group :development do
  gem 'typeprof', require: false
end
[/highlight]
```

Install it:

```command
bundle install
```

Create a directory for the source code:

```command
mkdir lib
```

Then create a simple Ruby file to analyze:

```ruby
[label lib/greeter.rb]
class Greeter
  def initialize(name)
    @name = name
  end

  def greet
    "Hello, #{@name}!"
  end

  def formal_greeting
    "Good day, #{@name}. How are you?"
  end
end
```

Run TypeProf on this file:

```command
bundle exec typeprof lib/greeter.rb
```

```text
[output]
# TypeProf 0.30.1

# lib/greeter.rb
class Greeter
  def initialize: (untyped) -> untyped
  def greet: -> String
  def formal_greeting: -> String
end
```

TypeProf analyzed the code and inferred that `@name` is a `String` based on how it's used in string interpolation. It also determined that `initialize` takes a `String` parameter and returns `void`, while the greeting methods return `String` values.

The inference works by tracking how values flow through your program. When TypeProf sees `"Hello, #{@name}!"`, it knows the result is a string, and therefore `greet` returns a string. When it sees `@name = name` in `initialize`, it infers that `name` must be a string to make the string interpolation valid.

Create a file that uses the `Greeter` class:

```ruby
[label lib/app.rb]
require_relative 'greeter'

greeter = Greeter.new("Alice")
puts greeter.greet
puts greeter.formal_greeting
```

Run TypeProf on both files:

```command
bundle exec typeprof lib/greeter.rb lib/app.rb
```

```text
[output]
# TypeProf 0.30.1

# lib/greeter.rb
class Greeter
  def initialize: (String) -> String
  def greet: -> String
  def formal_greeting: -> String
end

# lib/app.rb
```

The output remains the same because the usage in `app.rb` confirms TypeProf's inferences. The concrete value `"Alice"` validates that `initialize` expects a `String`.

This is TypeProf's basic workflow: you point it at Ruby files, it analyzes them, and it generates RBS signatures. The more context you provide through actual usage, the more confident TypeProf becomes about the types.

## How TypeProf infers types

TypeProf uses abstract interpretation to track how values move through your program. It doesn't execute your code - instead, it simulates execution symbolically, building up knowledge about what types exist at each point.

Let's see this in action with a method that performs calculations:

```ruby
[label lib/calculator.rb]
class Calculator
  def add(a, b)
    a + b
  end

  def double(x)
    add(x, x)
  end

  def sum_array(numbers)
    total = 0
    numbers.each do |num|
      total = add(total, num)
    end
    total
  end
end
```

Run TypeProf:

```command
bundle exec typeprof lib/calculator.rb
```

```text
[output]
# TypeProf 0.30.1

# lib/calculator.rb
class Calculator
  def add: (Complex | Float | Integer | Rational, untyped) -> (Complex | Float | Integer | Rational)
  def double: (untyped) -> (Complex | Float | Integer | Rational)
  def sum_array: (untyped) -> (Complex | Float | Integer | Rational)
end
```

TypeProf inferred that these methods work with integers, but how? It saw `total = 0` which is an integer literal, then tracked that `total` gets passed to `add` along with elements from the `numbers` array. Since `total` is an integer, the array elements must also be integers for `add` to work consistently.

The inference becomes more interesting when you give TypeProf concrete usage examples. Create a file that calls these methods:

```ruby
[label lib/calc_demo.rb]
require_relative 'calculator'

calc = Calculator.new
puts calc.add(5, 3)
puts calc.double(7)
puts calc.sum_array([1, 2, 3, 4, 5])
```

Run TypeProf on both files:

```command
bundle exec typeprof lib/calculator.rb lib/calc_demo.rb
```

```text
[output]
# TypeProf 0.30.1

# lib/calculator.rb
class Calculator
  def add: (Complex | Float | Integer | Rational, Integer) -> (Complex | Float | Integer | Rational)
  def double: (Integer) -> (Complex | Float | Integer | Rational)
  def sum_array: ([Integer, Integer, Integer, Integer, Integer]) -> (Complex | Float | Integer | Rational)
end

# lib/calc_demo.rb
```

The signatures match because the usage confirms the inferred types. TypeProf saw you passing integer literals and arrays of integers, which validates its analysis.

Now let's see what happens with methods that work with multiple types:

```ruby
[label lib/formatter.rb]
class Formatter
  def format(value)
    if value.is_a?(String)
      value.upcase
    elsif value.is_a?(Integer)
      value.to_s
    else
      value.inspect
    end
  end
end
```

Run TypeProf:

```command
bundle exec typeprof lib/formatter.rb
```

```text
[output]
# TypeProf 0.30.1

# lib/formatter.rb
class Formatter
  def format: (untyped) -> untyped
end
```

TypeProf falls back to `untyped` for the parameter because it can't determine from the code alone what types will actually be passed. The method handles multiple types differently, but without concrete usage, TypeProf stays conservative.

Provide usage examples to help TypeProf:

```ruby
[label lib/format_demo.rb]
require_relative 'formatter'

formatter = Formatter.new
formatter.format("hello")
formatter.format(42)
formatter.format([1, 2, 3])
```

Run TypeProf on both files:

```command
bundle exec typeprof lib/formatter.rb lib/format_demo.rb
```

```text
[output]
# TypeProf 0.30.1

# lib/formatter.rb
class Formatter
  def format: (Integer | String | [Integer, Integer, Integer]) -> String
end

# lib/format_demo.rb
```

Now TypeProf inferred a union type based on the actual usage patterns. It saw three different types being passed and generated a signature that accepts any of them.

This demonstrates an important principle: TypeProf's accuracy improves when you provide representative usage. The more your codebase shows how methods get called, the better TypeProf understands the intended types.

## Understanding TypeProf's limitations

TypeProf works remarkably well for straightforward Ruby code, but it has boundaries. Understanding where it struggles helps you know when to write signatures manually or refactor code for better inference.

Dynamic method definitions confuse TypeProf because it analyzes code statically. Here's an example:

```ruby
[label lib/dynamic.rb]
class DynamicClass
  [:foo, :bar, :baz].each do |name|
    define_method(name) do
      "Called #{name}"
    end
  end
end
```

Run TypeProf:

```command
bundle exec typeprof lib/dynamic.rb
```

```text
[output]
# TypeProf 0.30.1

# lib/dynamic.rb
class DynamicClass
end
```

TypeProf doesn't generate signatures for the dynamically defined methods because it can't determine at analysis time what methods will exist. The `define_method` call happens too dynamically for static analysis to follow.

For code like this, write the RBS signature manually:

```command
mkdir sig
```

```ruby
[label sig/dynamic.rbs]
class DynamicClass
  def foo: () -> String
  def bar: () -> String
  def baz: () -> String
end
```

Methods that use `method_missing` face similar issues:

```ruby
[label lib/proxy.rb]
class Proxy
  def method_missing(name, *args)
    "Proxied: #{name}"
  end

  def respond_to_missing?(name, include_private = false)
    true
  end
end
```

Run TypeProf:

```command
bundle exec typeprof lib/proxy.rb
```

```text
[output]
# TypeProf 0.30.1

# lib/proxy.rb
class Proxy
  def method_missing: (untyped, *untyped) -> String
  def respond_to_missing?: (untyped, ?false) -> true
end
```

TypeProf generates signatures for the actual methods defined, but it can't infer what methods `method_missing` will handle. You'll need to document those separately in RBS.

TypeProf also struggles with complex control flow and conditional logic:

```ruby
[label lib/complex.rb]
class Complex
  def process(input, mode)
    result = case mode
    when :double
      input * 2
    when :triple
      input * 3
    when :stringify
      input.to_s
    end
    
    result
  end
end
```

Run TypeProf:

```command
bundle exec typeprof lib/complex.rb
```

```text
[output]
# TypeProf 0.30.1

# lib/complex.rb
class Complex < Numeric
  def process: (untyped, untyped) -> nil
end
```

TypeProf inferred the return type is `(Integer | String)?` because the case statement might return `nil` if `mode` doesn't match any branch. It also knows that when the mode matches `:double` or `:triple`, an integer returns, but when mode is `:stringify`, a string returns.

The nullable return (`?` at the end) happens because TypeProf recognizes the case statement might not match anything. Fix this by adding an `else` clause:

```ruby
[label lib/complex.rb]
class Complex
  def process(input, mode)
    result = case mode
    when :double
      input * 2
    when :triple
      input * 3
    when :stringify
      input.to_s
    [highlight]
    else
      input
    [/highlight]
    end
    
    result
  end
end
```

Run TypeProf again:

```command
bundle exec typeprof lib/complex.rb
```

```text
[output]
# TypeProf 0.30.1

# lib/complex.rb
class Complex < Numeric
  def process: (untyped, untyped) -> untyped
end
```

Now the return type is no longer nullable because TypeProf knows the `else` branch ensures something always returns.

External dependencies also limit TypeProf's effectiveness. When your code calls methods from gems, TypeProf needs RBS signatures for those gems to understand the types:

```ruby
[label lib/http_client.rb]
require 'net/http'

class HttpClient
  def fetch(url)
    uri = URI(url)
    Net::HTTP.get(uri)
  end
end
```

Run TypeProf:

```command
bundle exec typeprof lib/http_client.rb
```

```text
[output]
# TypeProf 0.30.1

# lib/http_client.rb
class HttpClient
  def fetch: (untyped) -> untyped
end
```

TypeProf successfully inferred the signature because Ruby's standard library includes RBS signatures. If you use gems without RBS signatures, TypeProf will generate `untyped` for those interactions.

The key is recognizing when TypeProf's output needs refinement. Use it as a starting point, then review and improve the generated signatures based on your actual requirements.


## Final thoughts

This article showed you how TypeProf automatically generates RBS type signatures by analyzing Ruby code. The tool examines how values flow through your program and infers types without requiring annotations, making it practical to bootstrap type coverage quickly.

TypeProf works best with straightforward Ruby code where type flow is clear. Provide concrete usage examples to improve accuracy, and recognize when dynamic features like metaprogramming require manual RBS signatures. The combination of automated generation and targeted refinement creates comprehensive type documentation efficiently.

As Ruby's official type inference tool, TypeProf represents a low-friction path to static type checking. Generate signatures quickly, review them for accuracy, and maintain them alongside your code as it evolves.