コンテンツにスキップ

付録 — TypeScriptから来た場合

静的型チェッカーを見て「ああ、TypeScriptみたいなものか」と感じるなら、この付録でRigorの語彙をすでに知っているTypeScriptの概念にマッピングする。「TypeScriptは分かる」から「Rigorも分かる」への最短経路。

このページはチュートリアルではない。変換テーブルと、ふたつのシステムが本質的に異なる選択をしている箇所の簡単な考察だ — そこがTypeScriptで身についた反射的な理解の邪魔をする場所になる。

この付録の内容 5秒ピッチ · 型語彙マッピング · ナローイング · リファインメントキャリア · 「アノテーション不要」 · ジェネリクス · Null可能性 · 深刻度とstrictモード · TypeScriptにあってRigorにないもの · RigorにあってTypeScriptにないもの · マイグレーションvignette

問いTypeScriptRigor
アノテーションはどこに書くか?ソース内(x: number.rbの隣の.rbsファイル
誰が書くか?コードの作者作者OR推論
デフォルトは?any(TypeScript pre-strict)/ unknown(strict)精密に推論、不明ならDynamic[Top]
型の同一性構造的名前的 + 構造的ファセット
「まだ分からない」のコストアノテーションするまで赤い波線沈黙 — Dynamic[Top]は診断を出さない
診断が出るタイミング型が不健全なときRigorが不健全さを証明できるときだけ

ふたつのシステムは目標を共有する — プログラムを実行する前にバグを見つける — が、そこへの道筋で意見が分かれる。TypeScriptは健全性(soundness)優先のオーサリングを好む(すべての値が検査された型を持ち、そうなるまでチェッカーが文句を言う)。Rigorはfalse-positiveなし推論を好む(証明できないものには沈黙し、推論が見通せない箇所でだけ.rbsを求める)。

TypeScriptの形式Rigorの形式備考
stringString表示ではNominal[]を省略。
numberInteger / Float / NumericTSはintとfloatを同一視。Rigorはrubyのランタイムに従って分ける。
booleanbool(`ConstantConstant`)
nullnilConstant<nil>Rubyにはnilのみ。TSはnullundefinedを区別する。
undefined(対応なし)Rubyで未設定のローカル変数は「undefined」ではなくNameErrorになる。
anyDynamic[Top]「ここは黙っていて」キャリア(carrier)。
unknownTop両者ともナローイング(narrowing)するまでメソッドディスパッチを拒否。unknownDynamic[Top]よりTopに近い。
neverBot空の型 — 要素なし。到達不可能な分岐とT.absurd(Sorbet)/ raiseのみのボディに使う。
voidvoid同じ考え方 — 呼び出し元は値を消費してはならない。
`TU``T
T & UIntersection[T, U]Rigorではあまり一般的でない — リファインメント(refinement、篩型とも)で代替されることが多い。
"hello"(リテラル型)Constant<"hello">直接対応。Rigorではfoldingがより積極的。
42(リテラル型)Constant<42>同様。
`424344`
[number, string](タプル)Tuple[Integer, String]同じ位置ごとのモデル。
{ name: string; age: number }HashShape{name: String, age: Integer}同じキーごとのモデル。RubyはSymbolキーを慣習的に使う。
Array<T> / T[]Array[T]同様。
Record<K, V>Hash[K, V]同様。
Readonly<T>readonly_of[T](オプトインのrigor-typescript-utility-typesプラグイン経由)HashShapeの各エントリーに対するビュー層の読み取り専用マーカー。基底オブジェクトが凍結されていることを証明するものではない — ADR-13 §「Readonly」。
Partial<T> / Required<T>partial_of[T] / required_of[T](同じプラグイン)HashShapeの各エントリーの必須性を反転させる。Partialは値型をnil広げない — RigorのHashShapeは「キーが存在しない」と「キーは存在し値がnil」を区別する(ADR-13の必須性反転に関するWDを参照)。
Pick<T, K> / Omit<T, K>pick_of[T, K] / omit_of[T, K](同じプラグイン)リテラルキーユニオンでHashShapeのエントリーを制限/削除する。Tupleレシーバーは整数インデックスで射影。非シェイプ(shape)キャリアは保守的に縮退し、dynamic.shape.lossy-projectionを発火する。
条件型T extends U ? A : B(コアにはない。プラグインの提供で)プラグインは引数のシェイプによって戻り型を変えられる。
keyof T(なし)HashShapeは内部的にキーセットを公開するが型演算子としては公開しない。
T['k']T[k]インデックスアクセスRigorはHashShapeTupleのリテラルインデックスアクセスをサポートする(型仕様を参照)。
テンプレートリテラル型literal-stringキャリア「証明可能なリテラルから構築された」— 第2章参照。

ナローイング — 親しみやすい部分

Section titled “ナローイング — 親しみやすい部分”

TypeScriptのフローセンシティブ(flow-sensitive)なナローイングはRigorに直接対応するものがある。語彙は異なるが、動作は同じ。

TypeScriptRigor
if (x)if x — 真側のエッジからfalse/nilを除去
typeof x === "string"x.is_a?(String)
x instanceof Foox.is_a?(Foo)
x === nullx.nil?(およびx == nil
if (x !== null && x !== undefined)if x(Rubyにはnilのみでundefinedはない)
判別共用体switch (x.kind)case x; in {kind: :foo}またはcase x.kind; when :foo
ユーザー定義型ガードfunction isFoo(x): x is Foo%a{rigor:v1:predicate-if-true: x is Foo}ディレクティブ
asキャスト(コード内に対応なし) — Rigorはrigor-sorbet経由でT.cast、またはparam:ディレクティブを使用
x!(非nullアサーション)(コード内に対応なし) — rigor-sorbet経由でT.must、またはunless x.nil?ナローイング
as const定数は自動的にfoldされる — as constは不要

最大の実践的な違い: TypeScriptでは、チェッカーが同意しないときはいつでもas Fooに手が伸びる。Rigorにはソース内キャストがない。対応する方法は:

  1. ガードを追加するunless x.nil?; x.upcase; endが慣用的な手法。
  2. .rbsを絞り込む。問題の根本は大抵、ゆるすぎるライブラリのsig。
  3. rigor-sorbetプラグインを使う。ソース内アサーションが必要ならT.let/T.cast/T.mustを採用する。第10章参照。

リファインメントキャリア — TypeScriptにない部分

Section titled “リファインメントキャリア — TypeScriptにない部分”

TypeScriptは「長さ≥1の文字列」をテンプレートリテラル型かブランド型でしか表現できず、どちらも合成しにくい。Rigorにはファーストクラスのリファインメントキャリアがある — 証明可能な非空文字列、証明可能な正の整数、証明可能な非空配列。

RigorのリファインメントTypeScriptで最も近いものコメント
non-empty-string`${string}${string}`(テンプレートリテラルのトリック)またはブランドのNonEmptyStringTSでは不格好。Rigorはunless s.empty?から自動的に生成する。
positive-intブランドのPositiveIntTSユーザーはブランドをスキップしがち。Rigorはn > 0からナローイングする。
int<1, 9>リテラル型のunion `12
numeric-string(実用的なものなし)TSに対応なし。Rigorは数値パターンへの正規表現マッチからナローイングする。
non-empty-array[T][T, ...T[]](タプル+残余)TSにもエンコーディングはあるが使うAPIが少ない。Rigorはunless arr.empty?から生成する。

TypeScriptにnon-empty-stringがキーワードとして欲しかったことがあるなら、Rigorのこの部分を気に入るだろう。

「アノテーション不要」の実際

Section titled “「アノテーション不要」の実際”

典型的なTypeScriptのオンボーディング例:

function classify(n: number): "zero" | "positive" | "negative" {
if (n === 0) return "zero";
if (n > 0) return "positive";
return "negative";
}
const result = classify(7);
// TypeScript: result: "zero" | "positive" | "negative"

Rigorで対応するコード — アノテーションなし:

def classify(n)
return :zero if n.zero?
return :positive if n.positive?
:negative
end
result = classify(7)
assert_type("Constant<:zero> | Constant<:positive> | Constant<:negative>", result)

両チェッカーとも同じ正確なunionを推論する。TypeScriptバージョンはパラメータ型と戻り型のオーサードアノテーションが必要だが、Rigorバージョンはどちらも不要。

sigを書く必要があるとき — モジュール境界で、ボディが動的すぎるとき、パラメータシェイプを強制したいとき — それは.rbソースではなくsig/<file>.rbsに書く。この分離は意図的なもの(ADR-1とADR-5を参照)。

TypeScriptのジェネリクスは標準ライブラリの中心。Rigorのジェネリクスはより保守的なRBSのもの。RBSはクラスレベルの型パラメータとバウンド付き制約のメソッドレベル型パラメータをサポートするが、TypeScriptほど定型的なcall-site推論インスタンス化はまだサポートしていない。

TypeScriptRigor(RBS経由)
function id<T>(x: T): Tdef id: [T] (T) -> T
Array<T>Array[T]
Map<K, V>Hash[K, V]
Promise<T>(対応なし — Rubyにビルトインのpromiseはない)
Pick<T, K> / Omit<T, K> / Partial<T> / Required<T> / Readonly<T>オプトインのrigor-typescript-utility-typesプラグインが、HashShape上のpick_of / omit_of / partial_of / required_of / readonly_of(およびTuple上のpick_of / omit_of)に各々をマップする。
条件型(対応なし — プラグインが必要)

RigorはRBSジェネリクスをディスパッチャー経由で読み込み、レシーバーが十分な情報を持つcall-siteでパラメータをインスタンス化する。表示はRBSと同じ — Array[Integer]Array[Integer]と表示される。

TypeScriptのstrictNullChecksnullundefinedを独自の型にする。null可能はT | null | undefinedと書く。

Rubyにはnilしかない。RBSの省略形はT?で、T | nilに展開される。RigorのナローイングはnilをTypeScriptがnullを扱うのと全く同じように処理する:

def length(s) # s: String? (RBS宣言)
return 0 if s.nil?
s.length # s: String — .nil?チェックでnilが除去された
end

TypeScriptで対応するコードはほぼ同じ:

function length(s: string | null): number {
if (s === null) return 0;
return s.length;
}

深刻度、抑制、「strictモード」

Section titled “深刻度、抑制、「strictモード」”
TypeScriptRigor
tsconfig.jsonstrict: trueseverity_profile: strict
tsconfig.jsonnoImplicitAny(対応なし — Rigorはアノテーションを要求しない)
tsconfig.jsonstrictNullChecksRigorでは常にオン
// @ts-ignore# rigor:disable <rule>
// @ts-expect-error(現時点で対応なし)
// @ts-nocheck# rigor:disable-file all
tsc --noEmitrigor check lib

TypeScriptにあってRigorにないもの

Section titled “TypeScriptにあってRigorにないもの”

手放すものを正直に認識しておく:

  • 条件型T extends U ? A : BにコアのRigorの対応物はない。プラグインは引数のシェイプによって戻り型を変えられる(第9章参照)が、型レベルの式ではなくRubyコードで記述する。
  • マップ型PickOmitPartialRequiredReadonlyは、オプトインのプラグイン提供語彙としてrigor-typescript-utility-types経由で提供される。これらは、HashShape上のRigor正準シェイプ射影型関数pick_of / omit_of / partial_of / required_of / readonly_of(およびTuple上のpick_of / omit_of)にマップされる。テンプレートリテラル操作やその他のマップ型バリアント(Uppercase<S> / Lowercase<S> / Capitalize<S>)はRigorの表面の外にとどまる。
  • 型レベルの計算。TypeScriptの型システムはチューリング完全。Rigorのものは意図的にそうではない。これは制限ではなく特徴 — アナライザーは実際のRubyプロジェクトで高速でなければならない。
  • ソース内メソッドボディからの推論された戻り型tscは関数ボディから戻り型を推論し、呼び出し元に公開する。Rigorはソース内defに対して同じことをするが、RBS宣言されたメソッドは宣言された戻りに呼び出し元をバインドする — 意図的な境界規律disciplineの選択(ADR-5、堅牢性の原則を参照)。
  • エディタIntelliSenseの同等性。TypeScriptのツールには20年の投資がある。Rigorのエディタ統合は若い。現在のアナライザーは診断とrigor type-ofを提供し、LSP経由のエディタ統合はロードマップにある。

RigorにあってTypeScriptにないもの

Section titled “RigorにあってTypeScriptにないもの”

逆方向も:

  • ファーストクラスのリファインメントnon-empty-stringpositive-intnumeric-string等 — 述語で制限された値が自動的にナローイングされる。
  • メソッド呼び出しを通じた定数folding"foo".upcaseStringではなくConstant<"FOO">。Rigorはどのビルトインメソッドが純粋かをカタログ化し、それらを通じてfoldする。
  • false-positiveなしのスタンス。RigorはDynamic[Top]レシーバーには診断を出さずに沈黙する。「まあ、技術的にはチェッカーには分からない」が正解となる診断は決して出ない。
  • アノテーション税なし.rbsファイルがゼロのRubyプロジェクトでrigor checkを実行でき、推論だけから有用な診断を得られる。.rbsファイルの追加は段階的。スキップしたファイルは境界での診断ではなくDynamic[Top]になる。
  • 深刻度を意識した採用。TypeScriptの「all-or-nothing」感(strictをflipすると千のエラーが現れる)は、Rigorのlenient/balanced/strictプロファイルとルールごとのオーバーライドとベースラインdiffによって平滑化される。

TypeScriptのモジュールをRubyに移植している。元の関数:

function pick<K extends keyof T, T extends object>(obj: T, keys: K[]): Pick<T, K> {
const out = {} as Pick<T, K>;
for (const k of keys) {
if (k in obj) out[k] = obj[k];
}
return out;
}

Rigorのアプローチ:

lib/utils.rb
def pick(obj, keys)
keys.each_with_object({}) do |k, out|
out[k] = obj[k] if obj.key?(k)
end
end
sig/utils.rbs
def pick: [K, V] (Hash[K, V] obj, Array[K] keys) -> Hash[K, V]

RBSシグはジェネリックなままになる。Pick<T, K>の正確なキーセット追跡を取り戻したい場合は、rigor-typescript-utility-typesプラグインをオプトインし、戻り値型をPickの綴りで注釈する:

sig/utils.rbs
%a{rigor:v1:return: Pick[T, K]}
def pick: [K, V] (Hash[K, V] obj, Array[K] keys) -> Hash[K, V]

プラグインのTypeNodeResolverPick[T, K]を正準のpick_of[T, K]射影に変換する。どちらの場合でも、本当に重要なところでは呼び出しサイトは精密なまま — 呼び出しサイトのHashリテラルはシグネチャによらずHashShapeであり、キーごとの型はobj.key?(k)のナローイングを通じて保たれる。

この付録セクションの残りを順番に読む必要はおそらくない。3つの有用なポインタ:

他のツールと比較したい場合は、兄弟付録ページがPHPStanmypySteepTypeProfをカバーしている。

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.