TypeScriptは、プログラム実行前に型エラーを検出することで、開発者がクラッシュが起こらないコードを簡単に書けるようにするものです。私たちは開発者にこのツールを活用してもらいたいと考え、1年前に Cloudflare Workersランタイム用のTypeScript型を自動生成するシステムを構築しました。これにより開発者はWorkers API のコード補完をIDEで確認し、デプロイ前にコードをタイプチェックすることができるようになりました。毎週、 最新の変更を反映した新しいバージョンの型が公開されます 。
この1年間、お客様や社内のチームから、どのようにすれば型を改善できるかについてさまざまなフィードバックをいただきました。ランタイムのオープンソース化に向けてBazelビルドシステムに切り替えたことをきっかけに、さらに正確で使いやすく、生成もシンプルな型に作り直すことができたのです。本日、多数の新機能を備えた@cloudflare/workers-types
の次のメジャーリリースと、完全リライトの自動生成スクリプトのオープンソースを発表できることを嬉しく思います。
WorkerでのTypeScriptの使用方法
WorkersでTypeScriptをセットアップするのは簡単です。Workersを使い始めたばかりなら、Node.jsをインストールし、ターミナルでnpx wrangler init
を実行して、新しいプロジェクトを生成することができます。既存のWorkersプロジェクトがあり、改良された型付けを利用したい場合は、npm install --save-dev typescript @cloudflare/workers-types@latest
で最新バージョンのTypeScriptと@cloudflare/workers-types
をインストールし、以下の内容の tsconfig.json
ファイルを作成してください。
エディターが問題箇所を強調表示し、入力中にコード補完を行うことで、ミスを減り、開発環境が改善されます。
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"lib": ["esnext"],
"types": ["@cloudflare/workers-types"]
}
}
put
コマンドの代わりにset
コマンドの誤った使用を強調表示し、コード補完を行うエディタ
標準型による相互運用性向上
Cloudflare Workersはブラウザと同じランタイムAPIを多く実装しており、当社では WinterCGで標準準拠をさらなる改善を進めています。しかし、ブラウザと Workerでできることの間には、基本的な相違がかならず生じます。例えば、ブラウザはオーディオファイルを再生することができますが、Workersは世界各地に分散しているデータ保存のためにCloudflareのネットワークへ直接アクセスすることができるのです。このミスマッチは、各プラットフォームが提供するランタイム API と型が異なることを意味し、ひいてはCloudflareネットワークとブラウザで同じファイルを実行するRemixのようなフレームワークでWorkersの型を使用することを困難にしているのです。これらのファイルはlib.dom.d.ts
に対する型チェックが必要であり、当社の型と互換性がありません。
この問題を解決するために、tsconfig.json
のtypes
をフィールドに@cloudflare/workers-types
を含めなくても、選択的にインポートできる別バージョンのtypesを生成するようになりました。以下がその例です。
さらに、TypeScriptのlib.webworker.d.ts
に対する型のdiff を自動的に生成しています。今後、これを利用して、仕様準拠をさらに改善できる部分を特定していきます。
import type { KVNamespace } from "@cloudflare/workers-types";
declare const USERS_NAMESPACE: KVNamespace;
互換性のある日付での互換性を改善
Cloudflareでは、当社が提供しているすべてのAPIについて、強力な後方互換性のPromiseを維持しています。 互換性フラグと日付 を使用し、後方互換性のある方法でいくつかの変更を行います。これらの互換性フラグによって型が変更されることもあります。たとえば、global_navigator
フラグは新しいnavigator
globalを追加し、url_standard
フラグは URLSearchParams
コンストラクタのシグネチャを変更します。
互換性のある日付に合わせた型のバージョンを選択できるようになったので、実行時にサポートされない機能を使用していないことが確認できます。
Wranglerとの統合を改善
{
"compilerOptions": {
...
"types": ["@cloudflare/workers-types/2022-08-04"]
}
}
互換性の日付に加えて、Worker環境の設定もランタイムと型のAPIサーフェイスに影響を与えます。wrangler.toml
にKV namespacesやR2 bucketなどのバインディングが設定されている場合、それらをTypeScriptの型に反映させる必要があります。同様に、カスタムのテキスト、データ、WebAssemblyモジュールのルールも、TypeScriptがエクスポートのタイプを認識できるように宣言します。以前は、これらの宣言を含む別のアンビエントTypeScriptファイルを作成しなければなりませんでした。
wrangler.toml
をSSOTとして維持するために、npx wrangler types
を実行してこのファイルを自動的に生成することができるようになりました。
例えば、次のように wrangler.toml
...
...これらのアンビエント型を生成します。
kv_namespaces = [{ binding = "MY_NAMESPACE", id = "..." }]
rules = [{ type = "Text", globs = ["**/*.txt"] }]
統合されたドキュメントと変更履歴の改善
interface Env {
MY_NAMESPACE: KVNamespace;
}
declare module "*.txt" {
const value: string;
export default value;
}
コード補完により、Workersプラットフォームを初めて使う開発者はAPIサーフェスを便利に知ることができるようになりました。現在、当社はTypeScriptの公式型から、標準APIのドキュメントを型に含めるようにしました。 Cloudflare固有のAPIのドキュメントを型に含める作業も始めています。
すでにWorkersプラットフォームを使用している開発者にとっては、 @cloudflare/workers-types
の各リリースで型がどのように変化しているかを確認するのが難しいと感じることもあるでしょう。型エラーを回避し、新機能を強調するために、各リリースで新規、変更、削除された定義を分割した詳細な変更履歴を生成するようになりました。
型生成の仕組みについて
先に述べたように、当社では 自動型生成スクリプト をすべて再構築し、信頼性、拡張性、保守性をこれまで以上に高いものにしました。つまり、開発者はランタイムの新バージョン公開直後から、改良版の型を手に入れることができることになります。当社のシステムは現在、解析済みのC++ ASTからこうした情報を抽出するのではなく、workerd
の新しい実行時型情報(RTTI)システム を使用してWorkersランタイムAPIの型を照会しています。
このRTTIをTypeScriptプログラムに渡すと、TypeScript Compiler APIを使って宣言文を生成し、AST変換を行って整頓してくれます。これはworkerd
のBazelビルドシステムに組み込まれており、型の生成はbazel build //types:types
コマンド一つで済むようになりました。Bazelのキャッシュを利用して、生成時にできるだけ再構築しないようにしています。
// Encode the KV namespace type without any compatibility flags enabled
CompatibilityFlags::Reader flags = {};
auto builder = rtti::Builder(flags);
auto type = builder.structure<KvNamespace>();
capnp::TextCodec codec;
auto encoded = codec.encode(type);
KJ_DBG(encoded); // (name = "KvNamespace", members = [ ... ], ...)
自動生成された型はWorkersのランタイムAPIの JavaScriptインターフェイスを正しく記述していますが、TypeScriptは_より忠実な型_を提供し、開発者の人間工学の改善に利用できる追加機能が使えます。当社のシステムでは、自動生成された型にマージされるTypeScriptの「オーバーライド」を部分的に手書きで記述することができます。これにより、…
import ts, { factory as f } from "typescript";
const keyParameter = f.createParameterDeclaration(
/* decorators */ undefined,
/* modifiers */ undefined,
/* dotDotDotToken */ undefined,
"key",
/* questionToken */ undefined,
f.createTypeReferenceNode("string")
);
const returnType = f.createTypeReferenceNode("Promise", [
f.createUnionTypeNode([
f.createTypeReferenceNode("string"),
f.createLiteralTypeNode(f.createNull()),
]),
]);
const getMethod = f.createMethodSignature(
/* modifiers */ undefined,
"get",
/* questionToken */ undefined,
/* typeParameters */ undefined,
[keyParameter],
returnType
);
const kvNamespace = f.createInterfaceDeclaration(
/* decorators */ undefined,
/* modifiers */ undefined,
"KVNamespace",
/* typeParameters */ undefined,
/* heritageClauses */ undefined,
[getMethod]
);
const file = ts.createSourceFile("file.ts", "", ts.ScriptTarget.ESNext);
const printer = ts.createPrinter();
const output = printer.printNode(ts.EmitHint.Unspecified, kvNamespace, file);
console.log(output); // interface KVNamespace { get(key: string): Promise<string | null>; }
ReadableStream
などの型に型パラメータ(ジェネリック)を追加し、型付けされた値を一切使わないようにします。メソッドのオーバーロードで入出力型の対応を指定します。たとえば、
KVNamespace#get()
は、型引数がテキストの場合は文字列を返しますが、arrayBuffer
の場合はArrayBuffer
を返します。TypeScriptの標準に合わせ、型の名前を変更し、冗長性を低減します。
より正確な宣言のために、型を完全に置き換えます。例えば、
WebSocketPair
をconst
と置き換えて、Object.values()
でより良い型が得られるようにします。Request#cf
オブジェクトのような内部的に型付けされていない値に対して型を提供します。Workersで使えない内部型を非表示にします。
これらのオーバーライドはこれまで、オーバーライドするC++の宣言とは別のTypeScriptファイルで定義されていました。そのため、オリジナルの宣言と同期がとれない状況が頻繁に発生していました。新システムでは、オーバーライドはC++マクロでオリジナルと一緒に定義されるため、ランタイムの実装変更と一緒にレビューすることができます。README for workerd
's JavaScript glue code でさらに詳細と事例を見ることができます。
workersの型を使って実際にタイピングしてみましょう。
npm install --save-dev @cloudflare/workers-types@latest
で@cloudflare/workers-types
の最新バージョンにアップグレードし、新しいwrangler types
コマンドを試すことをお勧めします。workerd
のリリースごとに新バージョンの型を公開する予定です。Cloudflare Developers Discordでご意見をお聞かせください。また、改善すべきタイプを見つけた場合はGitHub issueを作成してください。