Rust製TypeScript Linterにおける型情報Lintルールの模索
Rust製TypeScript LinterであるBiome, Oxc, deno_lintなどは、TypeScriptの型情報を利用するLintルール(型情報Lintルール)[1]を持っていません。本記事では、その背景から、Rust製TypeScript Linterが型情報Lintルールを実現するための手段についてまとめます。
筆者のTSKaigi 2024で利用した下記登壇資料に情報を追加し、文章化したものです。
要約すると、以下になります。
Rust製TypeScript Linterは、安全性をさらに高めてくれるtypescript-eslintの型情報Lintルールが欲しいが、パフォーマンスを犠牲にしたくない。TypeScript Compilerに頼らずに実現するには、Alternative TypeScript Compilerや型推論が必要。型情報Lintルールの実装を型推論のサブセット構築によって行う場合、TypeScript 5.5 Betaで導入された
--isolatedDeclarations
による型注釈の強制が実現可能性を上げる?
ESLint + typescript-eslint
JavaScriptのデファクトスタンダードであるLinterは、ESLintです。
また、TypeScriptの利用が増えた現在、ESLintにTypeScript対応を拡張するPluginであるtypescript-eslintも広く使われるようになりました。
typescript-eslintの特徴は、大きく以下の2つです。
- ESLint PluginとしてTypeScript構文サポートを提供していること
- TypeScriptの型情報を用いたLintルールを提供していること
1つ目について、ESLint単体のParserでは、TypeScriptの構文をParseできないため、@typescript-eslint/parser
が必要になります。ESLintの設定を行う際には、よく記述すると思います。これによって、ESLint(typescript-eslint)は、TypeScript構文を解析してASTを作成できます。この段階で、構文を元にしたLintルールは実行可能になります。
2つ目について、typescript-eslintの大きな特徴かつ強みは、型情報Lintルールを提供していることです。型情報Lintルールは、TypeScriptのCompiler APIを用いて実装されており、安全性を高める上で強力です。
typescript-eslintの型情報Lintルール
型情報Lintルールについて、公式ドキュメントでは、Linting with Type Information | typescript-eslint で説明されています。Lint Ruleページで、"💭 type checked" で絞り込みを行って一覧を確認可能です。
いくつか特に有用と思うものに絞って簡単に紹介します。
- switch-exhaustiveness-check
- union型・enumがswitch文で全ケース網羅しているか検査するルール
- ハンドルされていないケースを防げるので、考慮・対応漏れなどを防ぐ
- switch-exhaustiveness-check | typescript-eslint
- strict-boolean-expressions
- boolean式で特定の型を許可しないルール
- {"allowString": false, "allowNumber": false, "allowNullableObject": false} で、ifのconditionに与えられる型をbooleanに強制できるため、評価を厳格化できる
- strict-boolean-expressions | typescript-eslint
- no-floating-promises
- Promiseを返す関数・メソッドが、適切にハンドルされているか検査するルール
- floating-promise(エラー処理されていない状態で作成されるPromise)の存在を検知することができるので、非同期処理のバグを防ぐ
- no-floating-promises | typescript-eslint
- 余談:BiomeのDiscussionなどで最も要望の多い型情報Lintルールです
型情報Lintルールは、TypeScript Compiler APIを用いて型情報を取得してLintを行うという性質上、どうしてもLint実行速度が犠牲になります。公式ドキュメントには、型情報Lintルールの利用は強く推奨しつつトレードオフを考慮するとよいと書かれています。
We strongly recommend you do use type-aware linting, but the above information is included so that you can make your own, informed decision. Linting with Type Information | typescript-eslint
余談:厳しいLintが好きな方やtypescript-eslintをフル活用したい方には、strictTypeChecked
というルールセットを使うのがおすすめです。recommended
には、型情報Lintルールが含まれていません。
Rust製TypeScript Linter : Biome, Oxc, deno_lint
昨今、Rust製のJavaScript / TypeScript Linterの利用が拡がっています。代表的なものとしては、Biome, Oxc, deno_lintが挙げられます。利用が拡大している理由として、ESLintと比較するとパフォーマンスがよいことが挙げられると思います。
それぞれのツールに関して簡単に箇条書きで紹介します。
Biome
- Web開発で利用する技術(JS/TS/JSON/CSSなど)を対象としたツールチェーン
- linter, formatter, analyzerを提供している(transformerも着手予定)
- ESLintや既存のPluginをサポートしているが、命名の統一・オプションの削減(デフォルトで厳しく)を意識している
Oxc
- JavaScript / TypeScript を対象としたツールチェーン
- linterを提供している(formatterやtransformerは作業中)
- ESLintや既存のPluginとの互換性を意識して開発が行われている
deno_lint
- Denoが提供しているDeno同梱のLinter
- ESLintとtypescript-eslintのrecommendedルールセットをサポート
BiomeとOxcの違いについては、Comparison with OXC というDiscussionをご参照ください。
Rust製TypeScript Linterにおいて型情報Lintルールがない理由
Rust製TypeScript Linterにおいて、型情報Lintルールがない理由は、主に以下の2つが考えられます。
- パフォーマンス
- これがRust製TypeScript Linterの主要な利点ですが、型情報LintルールをTypeScript Compilerに依存して実装する場合、パフォーマンスでの優位が失われる可能性があります
- 実装コスト(パフォーマンスを考慮する場合)
- 型情報を取得する際に、tsserver(TypeScript Compiler)に依存しない方法を考えると、実装コストの高いAlternative TypeScript CompilerやType Inferenceの実装が必要になります
これらの2つの理由から、現状のRust製TypeScript Linterは、型情報Lintルールを持っていませんでした。また既存のESLintや他Pluginのルールの実装も行う必要があるため、型情報Lintルールという重い実装よりも、他のルール実装にリソースを割いていました。
とはいえ、型情報Lintルールは要望が多いですし、もちろんRust製TypeScript Linterも実装したいです。
Rust製TypeScript Linterにおける型情報Lintルールの実装手段
実現手段として、主に以下の3つが考えられると思います。
- tsserver利用
- Alternative TypeScript Compilerの利用
- Type Inference (サブセット)の実装
1. tsserverの利用
tsserverは、本来テキストエディタのためにcode completionやrefactoringなどの機能を提供するLSPのように振る舞うサーバーです。
The TSServer is responsible for providing information to text editors. The TSServer powers features like code completion, refactoring tools and jump to definition. The TSServer is similar to the language server protocol but is older. TypeScript-Compiler-Notes/intro at main · microsoft/TypeScript-Compiler-Notes
このtsserverを介して型情報を取得する方法は、TypeScript Compilerにアクセスすることになるので、型情報は信頼できますが、速度はあまり期待できません。
Oxcには、以前このtsserverを利用する方法で no-floating-promisesを実装するPRが作成されていました。
実装の概観は、以下の図のようになるかと思います(togamiさん提供)。
Oxlintにおけるtsserverを利用したno-floating-promises実装の概観
しかし、Oxcの当該実装はマージ前にcloseされており、現在は後述するType Inferenceの実装が検討されています。
この1の方法は、LexerやParser, LSPなどの実行速度の差はあれど、TypeScript Compilerにアクセスするため、typescript-eslintと近い振る舞いとなります(tsserverを介するか、TypeScript Compiler APIを介するかの違い)。
2. Alternative TypeScript Compilerの利用
Alternative TypeScript Compiler は、その通りTypeScript Compilerの代替であり、stc (Rust) / ezno (Rust) / TypeRunner (C++) などがあります。ここでは、Rust製である stcとeznoを取り上げて紹介します。
stc (dudyktr/stc)
- 高速なTypeScript Type Checker
- lex と parseにはswcを利用しており、stcは型検査を行う
- tscの挙動を仕様として実装が行われており、独自の構文は未実装
- 2024年5月現在、開発は中止されている
Closing as the stc is now abandoned. TypeScript was not something that I could follow up on in an alternative language. TypeScript type checker · Issue #571 · swc-project/swc
元々は、swcに型検査プロセスを追加するために作成されていました。
より詳細な情報は、以下などを参照ください。
- Rust製TypeScriptコンパイラstcの現状と今後 | メルカリエンジニアリング
- Rewriting TypeScript in Rust? You'd have to be... | Total TypeScript
ezno (kaleidawave/ezno)
- Rust製のJavaScript Compiler & TypeScript Type Checker
- TypeScriptの型注釈を理解し、JavaScriptに対しても型検査を行う
- ソースコードから最大限の知識を得て、無効なプロパティの特定やデッドコードの検出なども行う
- stcとは異なり、TypeScriptの1-to-1の代替は目指していない(ただしTypeScriptとの互換は考えられており、振る舞いはその拡張といえると思う)
由来は、"(Is rewriting TypeScript Compiler) easy? no" とのことです(括弧は筆者追加)。
I think it is fun short and quirky and most importantly, not taken on package registries. It has a little bit of a hidden meaning ("easy? no" referring to doing static analysis on JavaScript). Introducing Ezno
より詳細な情報は、以下などを参照ください。
eznoは、過去のOxcと密に連携をとっており、Oxc側にもeznoを利用するためのcrateなどが存在していましたが、今はそれぞれ独立しているように見えます。
このAlternative TypeScript Compilerを使う方法の場合、Linter側のParserが生成するASTと Alternative TypeScript Compiler側のParserが生成するASTを用意して、Lint時に後者のASTにアクセスする流れになると思います。
双方のParserが同じであるなら、ASTとspan(コードの位置情報)をもとにそのアクセスが楽になると思いますが、現状あまりこの点は期待できない状態かもしれません。deno_lint(deno_ast)は、swcのastをベースにしていると思われるため[2]、stcが利用できる状態になっていれば、この辺りをスムーズに実装できたはずです。開発が続けられているeznoと他Linterの連携次第です。
tsc再実装の難しさ
tsc再実装の難しさとして以下が考えられると思います。
- TypeScriptの型システムの複雑性
- 仕様書が(v1.8以降)ないこと
- Microsoftの資本力によって開発されていること
1と2は挙動(と conformance tests)を元にして、実装力で解決できると思います。3もそう言えるといいですが、フルタイムで給与(paid by Microsoft)をもらっているエキスパートたちに追従することは、かなり大変だと思います。
最近、tscの再実装に関してmizdraさんがtsc の代替実装は作れるのか - mizdra's blogという記事が投稿されていました。
非常に野心的で面白いと思いつつ、正直僕は実用レベルまで達したものが本当に登場するのか疑問に思っている。今ある型システムもそうだし、新機能として追加されるものにも追従する必要がある。当然、実用レベルとして使ってもらうには、不具合も少なくないといけない。tsc の代替実装は作れるのか - mizdra's blog
自分も同意です。ただ、Linterを中心に考えて、そのLinterが欲しい型情報のみを得る実装に関しては現時点で可能性があると思っています。
余談:記事を書いている途中に、oven-shの@zack_overflow氏がTypeScript Type CheckerをRustで書いているとXで投稿しているのを見つけました。tyty というブログ記事にも書かれています。ただし、現在のステータスは不明で、ソースコードも確認できません。
3. Type Inferenceの実装
3つ目に、Type Inference(型推論)の実装が考えられます。
これは、実質的には、2. Alternative TypeScript Compiler の部分的な実装です。Rust製のTypeScript Linterがやりたいことは、型情報Lintルールの提供であり、TypeScriptの型検査までを行いたいわけではありません。型情報Lintルールによって、必要な型推論の範囲は異なります。
筆者は、この方法でどの程度Lintルールが機能するかについてはまだあまり想像できていません。Linterには、エラーを高精度で検出する能力が求められます。
パフォーマンスの観点から見ると、Linterが利用するASTを基に型推論を行うのであれば、他のアプローチと比べてParse回数などの点で有利です。
TypeScript 5.5 Betaでは、Rust製TypeScript Linterにとって有益になり得るCompiler Optionである--isolatedDeclarations
が導入されました。このオプションは、別ファイルからimportされた値の型の特定を容易にします。
--isolatedDeclarations
TypeScript 5.5 Betaで導入されたCompiler Optionsの1つに、--isolatedDeclarations
があります。これは「export対象のメンバーに十分な型注釈をつけるようにして、サードパーティーツールによって型定義ファイルを生成可能にする機能」です。コード上で、簡単に型推論が行えない型定義があった場合に、型注釈の追加を促すエラーが出されます。
--isolatedDeclarations
は、開発者に型注釈の追加を強制するため、導入はトレードオフだと思います。オプションをonにしている環境では、以下のような利益を得られます。
- より高速な宣言ファイル生成ツール(の開発・利用)
- 並列宣言ファイル生成・並列型検査
具体的には、--isolatedDeclarations
がONの場合、以下のように型注釈が求められます。
+ export const nOk: number = Math.random();
+ export function explicitReturnOk(a: number): void { }
+ export const valuesOk = [1, 2, 3] as const;
- export const nBad = Math.random();
- export function implicitReturnBad(a: number) { }
- export const valuesBad = [1, 2, 3];
より詳細は、以下Issueおよび--isolatedDeclarations
の主要開発者による発表などをご参照ください。
--isolatedDeclarations
がONになっている環境下において、Linter側は exportされた値の型を簡単に知ることができるようになります。(--isolatedDeclarations
をONにできるプロジェクトが実際にどれほどになるかは今後に委ねられます)
以下のようなコードで、型注釈が追加された場合、b.ts
に対するLintルール(no-floating-promises)の実行時には、returnPromise
関数の値を辿りさえすれば、型推論は不要になります。
// a.ts -----------------------------------------------------
+ export function returnPromise(): Promise<string> { 型注釈から型情報取得
- export function returnPromise() { // 返り値の型を推論する必要あり
return new Promise((resolve, reject) => {
setTimeout(() => resolve("data"), 1000);
});
}
// b.ts -----------------------------------------------------
import { returnPromise } from “a”;
returnPromise(); // Promise Likeな型でハンドリングされていないのでエラー
returnPromise() // Promise Likeな型だがハンドリングされているのでOK
.then(data => console.log(data))
.catch(err => console.error(err));
余談:multi-file analysis
上記の例のように、別ファイルで定義されている値の型を知るためには、複数ファイルを用いた解析処理が必要になります。いわゆるmulti-file analysisと呼ばれる機能で、Oxcはこれをサポートしています。
multi-file analysis の概要については、elm-reviewの作者であるjfmengels氏による以下の記事が詳しいです。
Rust製TypeScript Linterの今後
BiomeとOxcは、「3. Type Inferenceの実装」を取ろうとしています。
直近、BiomeのDiscordでBoshen氏とメンバーのコミュニケーションがありました。何かしらの形で連携することになるかもしれません(Parserはそれぞれ異なるものを使ってますが)。
deno_lintのステータスは、筆者が詳しくありません。no-floating-promisesルールに関しては、deno_lintのissueなどを見る限り、TypeScript本体にある async/await: nowait keyword? · Issue #13376 · microsoft/TypeScript というissueにて、no-floating-promisesと近しい機能が入ることを待っている状態だと思われます。ただ、このissueはかなり前に立てられており、現在あまりActiveには見えません。
Biome, Oxc, deno_lintの今後の方針
以上です。記事中に間違いがあれば修正するので、コメントください。BiomeのDiscordには、#type_inference チャンネルがあり時折アップデートがあるので、興味のある方は眺めてみてください。
今後のTypeScript, Alternative TypeScript Compiler, Rust製TypeScript Linterを要チェック!
References
- Rust-Based JavaScript Linters: Fast, But No Typed Linting Right Now
- Rewriting TypeScript in Rust? You'd have to be... | Total TypeScript
- Let's Make a Generic Inference Algorithm by Ryan Cavanaugh - GitNation
- Tour de Source: TypeScript ESLint - Sourcegraph
- Rust製TypeScriptコンパイラstcの現状と今後 | メルカリエンジニアリング
-
TypeScriptの型情報を用いて実装されたLintルールを指します。一般的な用語というより、typescript-eslint内で使われている用語を訳して名詞にしたものに過ぎません。 ↩︎
-
https://github.com/denoland/deno_ast/blob/main/Cargo.toml ↩︎
Discussion