担当しているRailsプロジェクトにrbs-inlinesteepによる型付け・型検査を導入しました。
検出された型エラーの調査・修正を行う中で、ruby/gem_rbs_collection 側の追加・修正が必要な箇所をいくつか見つけたので、初めて gem_rbs_collection にコントリビュートしてみました。

この記事は、GMOペパボ エンジニア Advent Calendar 2025 🎄会場 の11日目です。

gem_rbs_collection とは

gem_rbs_collection は、RBSを同梱していないgemのための型定義を集めた、コミュニティ管理のリポジトリです。

rbs collection init
rbs collection install

で簡単に導入でき、steepなどの型チェッカーと組み合わせて利用できます。

作成したPR

これまで合計3つのPRを出しました。

PR #871: GraphQL::ExecutionError#initialize の型定義追加

graphql: Add types for GraphQL::ExecutionError#initialize #871

初めてのPRです。 graphql-ruby gemの GraphQL::ExecutionError#initialize メソッドは ast_nodeoptionsextensions などの引数を受け取れますが、そもそもメソッドの型定義がなかったためにこれらの引数を渡すと型エラーが発生する状態でした。

test.rb:61:62: [error] Unexpected keyword argument
│ Diagnostic ID: Ruby::UnexpectedKeywordArgument
│
└       raise GraphQL::ExecutionError.new("Test error message", ast_node: nil, extensions: { "code" => "TEST_ERROR" })
                                                                ~~~~~~~~

test.rb:61:77: [error] Unexpected keyword argument
│ Diagnostic ID: Ruby::UnexpectedKeywordArgument
│
└       raise GraphQL::ExecutionError.new("Test error message", ast_node: nil, extensions: { "code" => "TEST_ERROR" })
                                                                               ~~~~~~~~~~

(これはテストコードをもとに当時の状況を再現したものです)

などを確認しつつ、以下のような型定義を追加しました。

+ def initialize: (String message, ?ast_node: untyped, ?extensions: Hash[untyped, untyped]?) -> void

通常の開発であれば単体テストなどで動作を保証できますが、型定義ファイル自体はそもそも実行するものではないため実行時の挙動に基づいたテストはできません。
代わりに gem_rbs_collection では、まずそのgemを利用した際のコードを用意し、それを型定義ファイルに基づいた型検査がpassすることをテストします。

テストとして用意した実際のコードが以下です。

def error_field
  raise GraphQL::ExecutionError.new("Test error message", ast_node: nil, extensions: { "code" => "TEST_ERROR" })
end

このコードをテストに追加すると型検査が落ちるようになるので、型定義を修正し、再び型検査が通るようになることを確認しました。つまり、TDDのサイクルで進められます。

ちなみにgem_rbs_collectionはレビュー無しでもマージできる運用になっていますが、初めてで心配だったので念のためレビューを依頼し、approveを頂いてからマージしました。元々はこのPRに options 引数も含めていたのですが、レビューにて deprecated であることを指摘頂き修正しました。(ありがとうございました!)

PR #887: RQRCode::QRCode#initialize の型定義修正

rqrcode: Fix type definition for RQRCode::QRCode#initialize #887

rqrcode gemの RQRCode::QRCode#initializegemのREADMEにもあるようにオプションを受け取れますが、これが型検査にてエラーとなっていました。

test.rb:6:45: [error] Unexpected keyword argument
│ Diagnostic ID: Ruby::UnexpectedKeywordArgument
│
└ qr = RQRCode::QRCode.new("https://kyan.com", level: :h)
                                               ~~~~~

gem の実装に合わせた型定義に修正しました。

- def initialize: (String) -> void
+ def initialize: (String, *untyped) -> void

PR #933: GraphQL::Schema::Resolver.null メソッド署名の追加

graphql: Add GraphQL::Schema::Resolver.null method signature #933

graphql-ruby gemの GraphQL::Schema::Resolver.null クラスメソッドの型定義がなかったため、追加しました。

test.rb:76:4: [error] Type `singleton(::Mutations::Ping)` does not have method `null`
│ Diagnostic ID: Ruby::NoMethod
│
└     null true
      ~~~~
+ def self.null: (?bool allow_null) -> untyped

注意すべき点

gem_rbs_collection では、gem ごとに対象とするバージョンが定められています。
PRを作り始める前に、そのメソッドや引数が追加・変更されたバージョンと、型定義の対象バージョンをチェックしておくとよいでしょう。

おわりに

gem側の型エラーはその場でパッチを用意して暫定対応することもできますが、以前 RubyKaigi 2022 の発表 Types teaches success, what will we do? で流れを聞いていたので、この機会にコントリビュートしてみることにしました。

実際にやってみると、ハードルは思ったより低かったです。型定義の不足や誤りを見つけたら、暫定対応だけでなくコントリビュートも選択肢に入れてみてください。