Sorbetの基本事項
- 漸進的型付け
- デフォルトでランタイムでも検査される(T.castなどいくつかのメソッドはランタイムのみで検査され、静的検査はされない。)
- 型定義のみのファイル(RBIというtsのd.tsのようなもの)があり、直接型定義が書けないものなどはRBIファイルを使用する
導入
Gemfile
1
2
3
| gem 'sorbet', :group => :development
gem 'sorbet-runtime'
gem 'tapioca', require: false, :group => :development
|
command
1
2
| bundle install
bundle exec tapioca init
|
静的チェックの有効化ついて
ファイル
ファイルの先頭にtyped: xxx
という形式で型の厳格度を指定する
緩い | | | | 厳しい |
---|
typed: ignore | typed: false | typed: true | typed: strict | typed: strong |
- typed: ignore: 型チェックなし。
- typed: false: シンタックス、定数が存在するかどうか、sigが正しいかどうかのみチェック。
- typed: true: 型チェックあり。
- typed: strict: すべてのメソッドにsigが必須。インスタンス変数は型の明示が必要。TypeScriptのnoImplicitAnyに近い。
- typed: strong: T.untypedが許可されない。ほぼ使われないのであまり気にしなくて良い。
メソッド
sigが記述されているメソッドは型検査される
1
2
3
4
5
6
7
8
9
10
11
| # typed: true
class Hoge
extend T::Sig
sig { params(a: Integer).void }
def hoge(a)
puts a
end
end
|
引数
T.untypedが指定されているものは型検査されない
1
2
3
4
5
6
7
8
9
10
11
| # typed: true
class Hoge
extend T::Sig
sig { params(a: T.untyped).void }
def hoge(a)
puts a
end
end
|
呼び出し側
T.unsafeで指定されているものは型検査されない
1
2
3
4
5
6
7
8
| # typed: true
class Hoge
define_method(:hoge) { puts 'hoge' }
end
hoge = Hoge.new
T.unsafe(hoge).hoge
|
型システム
基本的なメソッドの型定義方法について
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # typed: true
class Hoge
extend T::Sig # 各メソッドにsigを付与するために必要
sig { params(a: String).returns(String) } # 通常の引数、戻り値は使用(Rubyの作り上戻り値自体は必ずあるので、使用するかどうかという考え方になる)
def hoge(a)
a
end
sig { params(a: String).returns(String) } # キーワード引数、戻り値は使用
def fuga(a:)
a
end
sig { void } # 引数なし、戻り値は未使用
def piyo
puts 'hoge'
end
end
|
基本的な型一覧
型 | 例 | 補足 |
---|
String | “hoge” | |
Integer | 42 | |
Float | 3.14 | |
NilClass | nil | |
T::Array[T] | | ジェネリクスなのでTは型パラメータ |
T::Hash[T, U] | | ジェネリクスなのでT,Uは型パラメータ |
T::Set[T] | | ジェネリクスなのでTは型パラメータ |
T.nilable(A) | | Nilable Types. Aは任意の型 |
T.any(A, B, C) | | Union Types. A, B, Cは任意の型 |
T.all(A, B, C) | | Intersection Types. A, B, Cは任意の型 |
T.type_alias { A } | | 型エイリアス(Type Aliases). Aは任意の型 |
型アサーション(Type Assertions)
型の上書き。
比較的よく使用するもののみ記載
T.let
T.letとT.castは、TypeScriptのas相当。
下で記述しているT.castとの違いは静的な検査を行うこと。
T.cast
T.letとの違いは静的な検査が行われないこと。
1
| x = T.cast(10, Integer)
|
T.must
non null assertion.
TypeScriptの!相当。
1
2
3
| x = T.let("hoge", T.nilable(String))
T.must(x)
T.reveal_type(x) # => String
|
T.bind
self用のT.cast的なもの。
T.castと異なり代入は不要。
1
2
3
4
5
6
7
| class MyClass
def method_on_my_class
end
end
T.bind(self, MyClass) # これ以降selfがMyClass型として扱われる
self.method_on_my_class
|
T::Struct
HashだとT::Hash[KeyType, ValueType]
としか型が書けないので、T::Structを基本使用することになる。
1
2
3
4
5
6
7
8
9
| # typed: true
class Hoge < T::Struct
prop :hoge, Integer # propは再代入可能
const :fuga, Integer # constは再代入不可能。そもそもsetterがない
end
hoge = Hoge.new(hoge: 1, fuga: 1)
hoge.serialize # serializeメソッドを呼ぶとHashが返る
|
* 現状T::Structを継承したものを継承できない。つまり、上記のHogeを継承したクラスは作成できない。継承したい場合にはクラスで書くしかない。
T::Enum
1
2
3
4
5
6
7
8
9
10
11
12
| # typed: true
class A < T::Enum
enums do
Hoge = new('hoge') # newの引数には文字列が推奨されている。(you can pass any value to new (including things like Symbols or Integers). A future change to Sorbet may restrict this; we strongly recommend that you pass String values as the explicit serialization values.)と書かれている
Fuga = new('fuga')
Piyo = new('piyo')
end
end
Hoge::Hoge.serialize # => 'hoge' newに渡したものがserializeの戻り値となる。newの引数は省略することができるが、省略した場合には小文字の文字列が返ってくるのみ(スネークケースになったりはしないので基本明示的に指定するほうが無難)。
Hoge::Fuga.serialize # => 'fuga'
|
Blocks, Procs & Lambdas
T.procで記述する。
1
| T.proc.params(arg0: Arg0Type).returns(ReturnType)
|
罠系
selfを見失う。
1
2
3
4
5
6
| class Post
before_create :set_pending, if: -> {
T.bind(self, Post) # T.bindでselfの型を指定しないと下のdraft?は型エラーになる(Sorbetの仕様上)
draft?
}
end
|
Proc.newはProc型になってしまうので使用を避ける必要がある。procやlambda(->)が推奨。
1
2
3
4
| sig {returns(Proc)}
def proc_dot_new
Proc.new {|n| n * 2 }
end
|
Flow Sensitive Typing
よくある(?)条件分岐の型推論もある。
1
2
3
4
5
6
7
8
9
10
| # typed: true
extend T::Sig
x = T.let("hoge", T.nilable(String))
if x
T.reveal_type(x) # Revealed type: String
else
T.reveal_type(x) # Revealed type: NilClass
end
|
T.absurd
T.absurdが書いてあると、下記のように処理の記述漏れがある場合には型エラーになる。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # typed: true
class Hoge < T::Enum
extend T::Sig
enums do
A = new
B = new
C = new
end
def to_s
case self
when A then 'a'
when B then 'b'
else
T.absurd(self) # Cが処理されてないのでエラーとなる
end
end
end
|
T.class_of
クラスの型を記述するときに使用する。
1
2
3
| T.let(0, Integer) # ある型のインスタンスの型
T.let(Integer, T.class_of(Integer)) # T.class_ofがあればある型のクラスの型
|
T.self_type & T.attached_class
T.self_type
インスタンスメソッド利用時に自クラスを示す際に利用する。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # typed: true
class Parent
extend T::Sig
sig {returns(T.self_type)}
def foo
self
end
end
class Child < Parent; end
T.reveal_type(Parent.new.foo) # => Parent
T.reveal_type(Child.new.foo) # => Child
|
T.attached_class
クラスメソッド利用時に自クラスを示す際に利用する。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # typed: true
class Parent
extend T::Sig
sig {returns(T.attached_class)}
def self.make
new
end
end
class Child < Parent; end
T.reveal_type(Parent.make) # => Parent
T.reveal_type(Child.make) # => Child
|
Generics
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # typed: true
class Box
extend T::Sig
extend T::Generic # type_memberメソッドを提供する
Elem = type_member # Elemが型パラメータ
sig {params(val: Elem).void}
def initialize(val:)
@val = val
end
end
Box[Integer].new(val: 0)
|
Abstract Classes & Interfaces
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # typed: true
module Runnable
extend T::Sig
extend T::Helpers # 各種抽象クラスやインターフェースのメソッドを利用するために必要
interface! # 抽象クラスにする場合にはabstract!にする。abstract!を使用した場合には実装を持てるが、interface!の場合には実装は持てない
sig {abstract.params(args: T::Array[String]).void} # インターフェース側はabstract.paramsを使用する
def main(args); end
end
class HelloWorld
extend T::Sig
include Runnable
sig {override.params(args: T::Array[String]).void} # 実装側ではoverride.paramsを使用する
def main(args)
puts 'Hello, world!'
end
end
|
その他ここに全く書いてないもの
運用で叩くであろうコマンド群
コミュニティが管理している型定義やtapioca等が生成している型定義を常に更新しないといけないので下記コマンド群を常に叩くことになる。
1
2
3
4
5
| bin/tapioca annotations # コミュニティで管理されている型定義(RBI)の取得
bin/tapioca gem # gemのRBIの生成
bin/tapioca dsl # ActiveRecord等のRBIの生成. sorbet-railsを使用する場合にはここが bin/rails rails_rbi:all になる
bin/tapioca todo # TODOのRBIの生成
bundle exec srb rbi suggest-typed # RBIのコンフリクト解消
|
RBIについて
tapiocaで取得したり生成したRBIファイルは不完全なので、自分で定義を追加したり上書きしたりする必要がある。
追加する場合にはsorbet/rbi/shims以下にRBIファイルを置く。
詳細はこちら。
その他Tipsとかの知見
参考
Sorbet Docs
その他
なにか間違いを見つけた場合等はTwitter等でご連絡ください。