
February 2, 2026
Type Narrowing for Real-World Ruby Applications
Based on the RubyKaigi 2025 talk “Introducing Type Guard to Steep” by Takeshi Komiya CTO at Time Intermedia Inc., maintainer of rbs_rails and Rails type generators. Presented at RubyKaigi 2025.
Context and Attribution
This article is a technical analysis and expansion of the RubyKaigi 2025 conference talk “Introducing Type Guard to Steep”, presented by Takeshi Komiya (@tk0miya), a core contributor to the Ruby typing ecosystem and one of the maintainers of rbs_rails.
The original talk introduces Type Guard, a set of enhancements to Steep aimed at improving type narrowing in real-world Ruby and Rails applications. What follows is a deeper, written exploration of those ideas, with additional explanations and practical framing for production Ruby codebases.
Why Type Narrowing Matters in Ruby
Ruby is dynamically typed, but modern Ruby development increasingly relies on static analysis to improve safety, refactoring confidence, and long-term maintainability. Steep plays a central role in this ecosystem by validating Ruby code against RBS definitions.
A fundamental challenge Steep must solve is type narrowing—making a variable’s type more specific based on runtime checks.
user = find_user(id)puts user.name
If find_user returns User | nil, Steep correctly reports:
Type (User | nil) does not have method #name
This is not a false positive—it is an accurate reflection of the code’s potential behavior.
Built-in Type Narrowing in Steep
As explained in the RubyKaigi talk, Steep already supports type narrowing through a set of built-in constructs:
- nil checks
- is_a?, kind_of?, instance_of?
- logical operators (&&, ||)
- case / when
- !, ===, and related operators
Example:
if user.is_a?(AdminUser) user.admin_panelend
Inside the if block, Steep correctly narrows user to AdminUser.
However, this support only covered built-in Ruby semantics, not the patterns commonly used in Rails applications.
The Rails Reality Gap
Rails developers rarely rely on explicit nil checks. Instead, they use higher-level predicates such as present?:
if user.present? user.nameend
At runtime, this is perfectly safe. Historically, Steep did not narrow the type here, because present? was treated as a regular boolean method—not a type guard.
This mismatch between runtime meaning and static understanding is the core motivation behind Type Guard.
Type Guard: The Concept (RubyKaigi 2025)
In the talk, Type Guard is defined as:
An expression that allows Steep to narrow a variable’s type based on the semantic meaning of a predicate method.
A Type Guard must satisfy:
- The expression is a method call
- The receiver is a variable or a pure expression
- The receiver has a union type
- The return type of the method enables narrowing
This enables Steep to reason about predicates beyond Ruby core.
Type Guard for Union Types
A key example from the presentation is present?.
At runtime:
- User#present? → true
- NilClass#present? → false
With updated RBS definitions and Type Guard support (released in Steep 1.10), Steep can now infer:
if user.present? # user is Userelse # user is nilend
Steep evaluates each member of the union independently and narrows based on truthy vs falsey return types.
This change also included updates to ActiveSupport type definitions for present? and blank?.
Conservative Narrowing: A Deliberate Design
Not all predicates produce perfect narrowing.
Example:
value = gets # String | nil
if value.present? # value is Stringelse # value is String | nilend
Because String#present? can return both true and false, Steep intentionally remains conservative in the else branch. This behavior, highlighted in the talk, preserves soundness even when runtime intuition suggests otherwise.
User-Defined Type Guards
The second major contribution presented at RubyKaigi 2025 is User-defined Type Guards.
Real applications often rely on domain-specific predicates:
if user.admin? user.manage_systemend
Previously, Steep could not infer anything from admin?.
With user-defined Type Guards, developers can explicitly declare that:
When this method returns truthy, the receiver’s type should be narrowed.
This allows application logic—not just Ruby core or Rails—to guide type inference.
Design Constraints
As explained in the presentation, user-defined Type Guards are intentionally limited:
- They only narrow, never widen types
- The target type must be a subtype of the original
- Else-branch narrowing may fail for complex complements
These constraints ensure that Steep remains sound and predictable.
Why This Matters
The work presented by Takeshi Komiya at RubyKaigi 2025 represents a significant shift:
- Static typing adapts to Ruby idioms
- Framework and application logic are first-class citizens
- Fewer false positives, without weakening guarantees
Rather than forcing developers to write “type-checker-friendly Ruby”, Steep is learning to understand Ruby as it is actually written.

Conclusion
Type Guard brings Steep closer to real-world Ruby and Rails development.
By supporting:
- Union-aware predicates
- Framework-level semantics
- User-defined type narrowing
Steep becomes a more practical, expressive, and trustworthy tool—without compromising Ruby’s flexibility.
