既存のRailsプロジェクトにSorbetのRBSインラインコメントによる静的型付けを導入するまでにしたこと – ENECHANGE Developer Blog


こんにちは、エンジニアの細木です。

最近は寒くなってきたので煮込み料理をたくさん作っています。

以前のAIブログリレーでAIエージェント活用のためにやってみたいことの一つに型の導入を挙げました。

今回、既存のRuby on RailsプロジェクトにSorbetによる静的型付けの初期導入ができたので、技術選定からCIへの組み込みまでで実施したことをまとめます。

目的

静的型付けを導入した主な目的は以下の3つです。

1. 可読性向上

型情報によりコードの意図を明確化し、チーム全体の可読性を向上させます。「この引数は何を受け取るのか」「この戻り値は何を返すのか」といった情報が明示されることで、コードの理解が容易になります。

2. レビューコスト削減

型チェックによりコンパイル時エラー検出を実現し、レビュー時のバグ発見コストを削減します。これまでレビュアーが手動で推測していた型情報が明示されることで、レビューの焦点を実装ロジックに集中できます。

3. AI agentのガードレール、精度向上

型情報を活用してAI agentのコード生成精度を向上させ、不適切なコード生成を防止します。明確な型情報があることで、AIが生成するコードの品質が向上することを期待しています。

技術選定

Ruby on Railsプロジェクトに静的型付けを導入するにあたり、複数の選択肢を比較検討しました。

検討した選択肢

1. RBS + Steep

概要:

  • Rubyの標準規格として採用されているRBS
  • 型定義が別ファイル(.rbs)に分かれる方式

見送り理由:

  • 型定義が別ファイルに分かれるため、コードと型の対応を追うのが煩雑
  • インラインでの型付けという要件を満たさない

2. rbs-inline

概要:

  • RBS記法をインラインで記述できるツール
  • Ruby 3.3.0以上が必要

見送り理由:

  • Ruby version 3.3.0以上が必須だが、プロジェクトはRuby 3.0系を使用
  • バージョンアップのリスクとコストを考慮すると現時点では採用困難
  • 開発があまり活発でなく、プロダクション利用には安定性に懸念

github.com

3. Sorbet DSL (sig { … })

概要:

  • Sorbetの伝統的なインライン構文
  • tapiocaとの連携や実行時チェック機能が成熟

見送り理由:

  • Sorbet独自のDSLが冗長になりやすい(例: T.nilable(String)
  • コードの可読性を損なう可能性
  • 実行時オーバーヘッドがある

最終選択: Sorbet (#:) RBSコメント構文

複数の選択肢を比較検討した結果、Sorbetの#: (RBS) コメント構文を採用しました。

sorbet.org

採用理由

1. RBS記法による高い可読性

String?のように、Rubyコミュニティで標準化が進むRBSの簡潔な記法をそのまま使用できます。


class UserService
  
  def find_or_create(email, age, options = nil)
    
  end
end

2. インラインでの型付け

コードの隣に型情報を配置できるため、#:を読み飛ばせば通常のRubyコード、読めば型情報付きコードという二通りの読み方が可能です。

3. 実行時オーバーヘッドの排除

型定義がコメントとして扱われるため、アプリケーションの実行時性能に影響を与えず、ランタイム依存性を完全に排除できます。

4. 将来の標準規格への互換性

Rubyコミュニティでinline-rbsが標準機能としてマージされた場合でも、#:コメントの構文はRBSに準拠しているため、将来的な移行が非常に容易です。これはsig { ... }構文にはない大きな優位性です。

制約事項

選定にあたり、以下の制約も理解した上で採用を決定しました。

  • #:コメントは実験的機能(--enable-experimental-rbs-comments
  • 実行時チェック機能はなし(静的解析のみ)
  • Rubyの高度なメタプログラミングには対応困難な場合がある

やったこと

1. Sorbetのセットアップ

Gemの追加

group :development do
  gem 'sorbet', require: false
  gem 'sorbet-runtime'
  gem 'tapioca', require: false
end
  • sorbet: 型チェッカー本体
  • sorbet-runtime: ランタイム型アサーションに必要
  • tapioca: RBIファイル(型定義ファイル)の自動生成ツール

Sorbet設定ファイルの作成

# Tapiocaの初期化(sorbet/config等の設定ファイルを自動生成)
bundle exec tapioca init

これにより、sorbet/configファイルが自動生成されます。生成された設定ファイルに--enable-experimental-rbs-commentsを追加します。

# sorbet/config
--dir=.
--ignore=/tmp/
--ignore=/vendor/bundle
--ignore=/spec
--ignore=/config
--enable-experimental-rbs-comments

重要なポイント:

  • --enable-experimental-rbs-comments: RBS記法の型コメントを有効化
  • /spec/configは型チェック対象外(テストコードは型チェック不要)
  • --ignoreの副作用に注意: ignoreディレクトリを指定すると、そのディレクトリ内のコードが型チェックされないだけでなく、他のファイルから型として参照することもできなくなります。必要最小限のディレクトリのみをignoreするように注意が必要です

2. RBIファイルの生成

Sorbetが型チェックを行うには、Gemやライブラリの型定義(RBIファイル)が必要です。

# Gemの型定義を生成
bundle exec tapioca gems

# Rails DSLの型定義を生成(ActiveRecordモデル等)
bundle exec tapioca dsl

これにより、sorbet/rbi/gems/sorbet/rbi/dsl/配下に大量のRBIファイルが自動生成されます。

3. 全ファイルへのtyped指定

Sorbetでは対象のディレクトリ配下のすべてのファイルで型チェックをおこいます。
そのため段階的導入を可能にするため、既存の全ファイルに# typed: falseを追加し明示的に型チェックの対象から除外しました。
これにより、既存コードへの影響を最小限に抑えつつ、ファイル単位で型付けを進められる基盤を構築しました。

別のオプションで# typed: ignoreもありますが、これはsorbet/config--ignore設定と同様にSorbetから認識されなくなるため他のファイルから型として参照することもできなくなります。

4. 型コメントの実装例

まず1ファイルで実際に型を付け、動作確認を行いました。


class UserService
  
  def find_or_create(email, age, options = nil)
    
  end
end
  • # typed: strict: ファイル全体で厳格な型チェックを有効化
  • #:: メソッドシグネチャを定義
  • ?Hash[Symbol, String]?: オプショナル引数でnilable型
  • User?: nilableな戻り値(ユーザーが見つからない場合はnil)

5. RuboCop設定の調整

Sorbet/RBSの型コメント(#:)は#の直後にスペースがないため、RuboCopのLayout/LeadingCommentSpaceルールに抵触します。

Layout/LeadingCommentSpace:
  Enabled: false

プロダクトに導入されていたのRuboCopバージョンではAllowRBSInlineAnnotationパラメータがサポートされていないため、一時的に完全無効化しています。

6. CI/CD統合

CIパイプラインにSorbet型チェックステップを追加しました。

- label: "sorbet type check"
  commands:
    - bundle install
    - bundle exec srb tc

CI統合での課題とトレードオフ

元々はCI環境でtapioca gems, tapioca dls によるRBIファイルを生成しようとしていましたが、実行時にデータベース接続エラーが発生してしまいました。

Error: `Tapioca::Dsl::Compilers::ActiveRecordColumns` failed to generate RBI
ActiveRecord::NoDatabaseError: database does not exist

tapioca dslはActiveRecordモデルを読み込んでメソッドを解析するため、データベース接続が必要ですが、CI環境での安定したデータベース接続が困難でした。

採用した解決策:

RBIファイルをgitリポジトリにコミットする方式を採用しました。

  • 開発環境でtapioca gemstapioca dslを実行
  • 生成されたRBIファイルをgitにコミット
  • CI環境では型チェック(srb tc)のみ実行

トレードオフ:

✅ メリット:

  • CIがシンプルで高速
  • データベース接続不要
  • CI環境での技術的問題を回避

❌ デメリット:

  • 自動生成可能なファイル(約200個)をリポジトリに含める
  • ベストプラクティスではない可能性がある
  • PRの差分に大量のRBIファイルが含まれる可能性

この課題は今後の改善事項として記録しており、CI環境でのRBI生成を実現する方法を継続的に検討していきます。

まとめ

既存のRailsプロジェクトにSorbet(RBSコメント)による静的型付けを導入し、以下を達成しました。

  • ✅ 可読性向上とレビューコスト削減の基盤構築
  • ✅ CI/CDパイプラインへの型チェック統合
  • ✅ 段階的に型安全性を向上できる仕組みの実現

完璧な実装ではなくトレードオフを含む判断もありましたが、型チェックの導入というゴールを優先し、改善課題を明確にすることで、今後の継続的な改善の道筋を立てることができました。

静的型付けの導入を検討している方の参考になれば幸いです。

参考資料


元の記事を確認する

関連記事