SorbetはRubyのシンタックスで書かれているため、型アノテーションの情報を実行時にも保持できます。
(Steepはコメントアウトで型アノテーションを記述するため、実行時には型の情報が捨てられてしまっているようです)
これを用いて、実行時に型アノテーションの検査ができるというので、手元で試してみました。
Sorbet Runtimeについて
Sorbetは部分的に型チェックを有効化、無効化することが可能です。そのため、型アノテーションを記述しても異なる型の値が渡される可能性があります。
Sorbet Runtimeは実行時に型情報を用いて、アノテーションをチェックすることができます。
これにより、型アノテーションの誤りに気づくことができます。
試してみる
まずは型チェックを無効化してプログラムを実行してみます。
# typed: false
class Untyped
attr_reader :age
def initialize(age:)
@age = age
end
def string_age
age.to_s
end
end
require 'rails_helper'
RSpec.describe Untyped do
describe '#string_age' do
subject { described_class.new(age: age).string_age }
context 'when age is Integer' do
let(:age) { 20 }
it { is_expected.to eq('20') }
end
context 'when age is not Integer' do
let(:age) { nil }
it { is_expected.to eq('') }
end
end
end
エラーが起きていないことが確認できました。
型チェックを有効化
次に型アノテーションを記述して、型チェックを有効化します
# typed: strict
class Typed
extend T::Sig
sig { returns(Integer) }
attr_reader :age
sig { params(age: Integer).void }
def initialize(age:)
@age = T.let(age, Integer)
end
sig { returns(String) }
def string_age
age.to_s
end
end
するとエラーが発生しました。
ageにIntegerを期待していたのにNilClassを受け取ったことでエラーになっています。そしてTypeErrorがraiseされています。
ランタイムチェックを無効化したいとき
デフォルトではすべての環境においてランタイムチェックが実行されます。
これを変更したり、無効化したりするには設定や環境変数が必要です。
T::Configuration.default_checked_level = :tests # default :always
環境変数の場合は SORBET_RUNTIME_DEFAULT_CHECKED_LEVEL
を使います。
以下のようにアノテーションされているコードに対して、 SORBET_RUNTIME_DEFAULT_CHECKED_LEVEL=never
で実行します。
# typed: true
class Typed
extend T::Sig
sig { returns(Integer) }
attr_reader :age
sig { params(age: Integer).void }
def initialize(age:)
@age = age
end
sig { returns(String) }
def string_age
age.to_s
end
end
このようにエラーが発生しませんでした。
しかし、T.let
, T.cast
, T.must
, T.bind
といった型アサーションを使った場合、必ずチェックされるので注意が必要です。
# typed: strict
class Typed
extend T::Sig
sig { returns(Integer).checked(:never) }
attr_reader :age
sig { params(age: Integer).void.checked(:never) }
def initialize(age:)
+ @age = T.let(age, Integer)
end
sig { returns(String).checked(:never) }
def string_age
age.to_s
end
end
このように型アサーションは常にRubyコードとして実行されるため、わずかながらオーバーヘッドがかかるようです。そのため、ドキュメントにも型アサーションを使わず、メソッドシグニチャを使う方法も紹介されています。
終わりに
RSpec等を実行したときに型アノテーションのチェックができるというのはとてもいいと思いました。
YARDからRBIを生成するツールもあるので、それで生成した後にSorbet Runtimeでチェックするとかを今度試してみようと思いました。