Zenn
🎃

[Sorbet] requires_ancestorでmoduleが特定のクラスを継承したクラスにincludeされることを保証する

2025/02/24に公開

特定のクラスを継承したクラスで使われることを期待したメソッドを切り出したようなmoduleを作ることは良くあります。
RubyにSorbetで型を当てている場合は、requires_ancestorという機能を使うことでエラーを回避できる場合があるため紹介します。

問題

例えば、以下のようなコードがあったとします。(Concernsを意識したコードにしてみました。)

class ApplicationController
  def base_method
  end
end

module Countable
  def count
    base_method # -> Method base_method does not exist on Countable 
  end
end

class FooController < ApplicationController
  include Countable
end

FooControllerがApplicationControllerを継承していて、Countableをincludeしています。
module Countableの内部ではApplicationController内のbase_methodに依存しています。

しかし、Sorbetはそのmoduleをincludeする側のクラスの情報は知らないため、base_methodを呼び出している部分でエラーになってしまいます。

解決方法

requires_ancestorという機能を使い、moduleをincludeするクラスが特定のクラスを継承していることを保証します。
moduleにrequires_ancestor { xxx }でApplicationControllerを指定することで、Countable内でbase_methodを呼び出すことができます。

class ApplicationController
  def base_method
  end
end

module Countable
  extend T::Helpers

  requires_ancestor { ApplicationController }

  def count
    base_method # No error
  end
end

class FooController < ApplicationController
  include Countable
end

以下のリンクで動作を確認できます。
Sorbet Playground

補足事項

requires_ancestorは実験的機能なので、CLIで--enable-experimental-requires-ancestorオプションを指定する必要があります。
ファイルに書く場合はsorbet/configに一行加えます。

Sorbet Playgroundでは、URLに?arg=--enable-experimental-requires-ancestorを加えることで有効にできます。
(左のエディタではうまく反映されずにエラーの波線が出てしまいますが、右のエリアではエラーが消えていることがわかります。)

よく使う場面と対応方法

最後に、requires_ancestorを良く使う場面と対応方法をメモしておきます。

  • ModelやControllerに対するConcerns
    • requires_ancestor { ApplicationController }
    • requires_ancestor { ApplicationRecord }
  • raiseが解決できないファイル
    • requires_ancestor { Kernel }

Discussion

ログインするとコメントできます