付録 — TypeScriptから来た場合
静的型チェッカーを見て「ああ、TypeScriptみたいなものか」と感じるなら、この付録でRigorの語彙をすでに知っているTypeScriptの概念にマッピングする。「TypeScriptは分かる」から「Rigorも分かる」への最短経路。
このページはチュートリアルではない。変換テーブルと、ふたつのシステムが本質的に異なる選択をしている箇所の簡単な考察だ — そこがTypeScriptで身についた反射的な理解の邪魔をする場所になる。
この付録の内容 5秒ピッチ · 型語彙マッピング · ナローイング · リファインメントキャリア · 「アノテーション不要」 · ジェネリクス · Null可能性 · 深刻度とstrictモード · TypeScriptにあってRigorにないもの · RigorにあってTypeScriptにないもの · マイグレーションvignette
| 問い | TypeScript | Rigor |
|---|---|---|
| アノテーションはどこに書くか? | ソース内(x: number) | .rbの隣の.rbsファイル |
| 誰が書くか? | コードの作者 | 作者OR推論 |
| デフォルトは? | any(TypeScript pre-strict)/ unknown(strict) | 精密に推論、不明ならDynamic[Top] |
| 型の同一性 | 構造的 | 名前的 + 構造的ファセット |
| 「まだ分からない」のコスト | アノテーションするまで赤い波線 | 沈黙 — Dynamic[Top]は診断を出さない |
| 診断が出るタイミング | 型が不健全なとき | Rigorが不健全さを証明できるときだけ |
ふたつのシステムは目標を共有する — プログラムを実行する前にバグを見つける — が、そこへの道筋で意見が分かれる。TypeScriptは健全性(soundness)優先のオーサリングを好む(すべての値が検査された型を持ち、そうなるまでチェッカーが文句を言う)。Rigorはfalse-positiveなし推論を好む(証明できないものには沈黙し、推論が見通せない箇所でだけ.rbsを求める)。
型語彙マッピング
Section titled “型語彙マッピング”| TypeScriptの形式 | Rigorの形式 | 備考 |
|---|---|---|
string | String | 表示ではNominal[]を省略。 |
number | Integer / Float / Numeric | TSはintとfloatを同一視。Rigorはrubyのランタイムに従って分ける。 |
boolean | bool(`Constant | Constant |
null | nil(Constant<nil>) | Rubyにはnilのみ。TSはnullとundefinedを区別する。 |
undefined | (対応なし) | Rubyで未設定のローカル変数は「undefined」ではなくNameErrorになる。 |
any | Dynamic[Top] | 「ここは黙っていて」キャリア(carrier)。 |
unknown | Top | 両者ともナローイング(narrowing)するまでメソッドディスパッチを拒否。unknownはDynamic[Top]よりTopに近い。 |
never | Bot | 空の型 — 要素なし。到達不可能な分岐とT.absurd(Sorbet)/ raiseのみのボディに使う。 |
void | void | 同じ考え方 — 呼び出し元は値を消費してはならない。 |
| `T | U` | `T |
T & U | Intersection[T, U] | Rigorではあまり一般的でない — リファインメント(refinement、篩型とも)で代替されることが多い。 |
"hello"(リテラル型) | Constant<"hello"> | 直接対応。Rigorではfoldingがより積極的。 |
42(リテラル型) | Constant<42> | 同様。 |
| `42 | 43 | 44` |
[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はHashShapeとTupleのリテラルインデックスアクセスをサポートする(型仕様を参照)。 |
| テンプレートリテラル型 | literal-stringキャリア | 「証明可能なリテラルから構築された」— 第2章参照。 |
ナローイング — 親しみやすい部分
Section titled “ナローイング — 親しみやすい部分”TypeScriptのフローセンシティブ(flow-sensitive)なナローイングはRigorに直接対応するものがある。語彙は異なるが、動作は同じ。
| TypeScript | Rigor |
|---|---|
if (x) | if x — 真側のエッジからfalse/nilを除去 |
typeof x === "string" | x.is_a?(String) |
x instanceof Foo | x.is_a?(Foo) |
x === null | x.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にはソース内キャストがない。対応する方法は:
- ガードを追加する。
unless x.nil?; x.upcase; endが慣用的な手法。 .rbsを絞り込む。問題の根本は大抵、ゆるすぎるライブラリのsig。rigor-sorbetプラグインを使う。ソース内アサーションが必要ならT.let/T.cast/T.mustを採用する。第10章参照。
リファインメントキャリア — TypeScriptにない部分
Section titled “リファインメントキャリア — TypeScriptにない部分”TypeScriptは「長さ≥1の文字列」をテンプレートリテラル型かブランド型でしか表現できず、どちらも合成しにくい。Rigorにはファーストクラスのリファインメントキャリアがある — 証明可能な非空文字列、証明可能な正の整数、証明可能な非空配列。
| Rigorのリファインメント | TypeScriptで最も近いもの | コメント |
|---|---|---|
non-empty-string | `${string}${string}`(テンプレートリテラルのトリック)またはブランドのNonEmptyString | TSでは不格好。Rigorはunless s.empty?から自動的に生成する。 |
positive-int | ブランドのPositiveInt | TSユーザーはブランドをスキップしがち。Rigorはn > 0からナローイングする。 |
int<1, 9> | リテラル型のunion `1 | 2 |
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? :negativeend
result = classify(7)assert_type("Constant<:zero> | Constant<:positive> | Constant<:negative>", result)両チェッカーとも同じ正確なunionを推論する。TypeScriptバージョンはパラメータ型と戻り型のオーサードアノテーションが必要だが、Rigorバージョンはどちらも不要。
sigを書く必要があるとき — モジュール境界で、ボディが動的すぎるとき、パラメータシェイプを強制したいとき — それは.rbソースではなくsig/<file>.rbsに書く。この分離は意図的なもの(ADR-1とADR-5を参照)。
ジェネリクス
Section titled “ジェネリクス”TypeScriptのジェネリクスは標準ライブラリの中心。Rigorのジェネリクスはより保守的なRBSのもの。RBSはクラスレベルの型パラメータとバウンド付き制約のメソッドレベル型パラメータをサポートするが、TypeScriptほど定型的なcall-site推論インスタンス化はまだサポートしていない。
| TypeScript | Rigor(RBS経由) |
|---|---|
function id<T>(x: T): T | def 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]と表示される。
Null可能性
Section titled “Null可能性”TypeScriptのstrictNullChecksはnullとundefinedを独自の型にする。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が除去されたendTypeScriptで対応するコードはほぼ同じ:
function length(s: string | null): number { if (s === null) return 0; return s.length;}深刻度、抑制、「strictモード」
Section titled “深刻度、抑制、「strictモード」”| TypeScript | Rigor |
|---|---|
tsconfig.jsonのstrict: true | severity_profile: strict |
tsconfig.jsonのnoImplicitAny | (対応なし — Rigorはアノテーションを要求しない) |
tsconfig.jsonのstrictNullChecks | Rigorでは常にオン |
// @ts-ignore | # rigor:disable <rule> |
// @ts-expect-error | (現時点で対応なし) |
// @ts-nocheck | # rigor:disable-file all |
tsc --noEmit | rigor check lib |
TypeScriptにあってRigorにないもの
Section titled “TypeScriptにあってRigorにないもの”手放すものを正直に認識しておく:
- 条件型。
T extends U ? A : BにコアのRigorの対応物はない。プラグインは引数のシェイプによって戻り型を変えられる(第9章参照)が、型レベルの式ではなくRubyコードで記述する。 - マップ型。
Pick、Omit、Partial、Required、Readonlyは、オプトインのプラグイン提供語彙として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宣言されたメソッドは宣言された戻りに呼び出し元をバインドする — 意図的な境界規律の選択(ADR-5、堅牢性の原則を参照)。 - エディタIntelliSenseの同等性。TypeScriptのツールには20年の投資がある。Rigorのエディタ統合は若い。現在のアナライザーは診断と
rigor type-ofを提供し、LSP経由のエディタ統合はロードマップにある。
RigorにあってTypeScriptにないもの
Section titled “RigorにあってTypeScriptにないもの”逆方向も:
- ファーストクラスのリファインメント。
non-empty-string、positive-int、numeric-string等 — 述語で制限された値が自動的にナローイングされる。 - メソッド呼び出しを通じた定数folding。
"foo".upcaseはStringではなく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によって平滑化される。
マイグレーションvignette
Section titled “マイグレーションvignette”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のアプローチ:
def pick(obj, keys) keys.each_with_object({}) do |k, out| out[k] = obj[k] if obj.key?(k) endenddef pick: [K, V] (Hash[K, V] obj, Array[K] keys) -> Hash[K, V]RBSシグはジェネリックなままになる。Pick<T, K>の正確なキーセット追跡を取り戻したい場合は、rigor-typescript-utility-typesプラグインをオプトインし、戻り値型をPickの綴りで注釈する:
%a{rigor:v1:return: Pick[T, K]}def pick: [K, V] (Hash[K, V] obj, Array[K] keys) -> Hash[K, V]プラグインのTypeNodeResolverがPick[T, K]を正準のpick_of[T, K]射影に変換する。どちらの場合でも、本当に重要なところでは呼び出しサイトは精密なまま — 呼び出しサイトのHashリテラルはシグネチャによらずHashShapeであり、キーごとの型はobj.key?(k)のナローイングを通じて保たれる。
次のステップ
Section titled “次のステップ”この付録セクションの残りを順番に読む必要はおそらくない。3つの有用なポインタ:
- 第2章 — 日常的に出会う型 — リファインメントを見たことがない場合のキャリアの種類。
- 第7章 — RBSと
RBS::Extended— ディレクティブ文法(カスタム型述語をRigorに教える方法)。 - 第10章 — Sorbetとの共存 — プロジェクトが実際にSorbetをすでに使っている場合。
T.let、T.cast、T.mustに直接対応するものがあり、ゼロから始めるよりスムーズに移行できる。
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.