これまでは、RustおよびTypeScriptのタイプリポジトリを最新に保つことは困難でした。それらは手動で生成されたため、その結果不正確または期限切れになるリスクがありました。最近まで、workers-typesのリポジトリは、タイプが変更されるたびに手動で更新する必要がありました。また、ほとんど完全なブラウザAPIにタイプ情報を追加する際にも使用していました。このため、ユーザーがWorkersのランタイムでサポートされていないブラウザAPIを使用しようとすると、コンパイルしてもエラーが発生し、混乱が生じていました。

この夏、Brendan Coll氏が当社のインターンとして働いていた間に、それらを生成するための自動化パイプラインを作成したときに、すべてが変わりました。Workers ランタイムを構築するたびにそれが実行され、TypeScriptリポジトリおよび Rustリポジトリのタイプが生成されます。いまでは、すべてが最新かつ正確になります。

簡単な概要

Workersランタイムコードが作成されるたびに、パブリックAPI上でスクリプトが実行され、Rustの型とTypeScriptの型、静的型の中間表現を含むJSONファイルと共に生成されます。これらの型は適切なリポジトリに送信され、ユーザーが独自の型パッケージを作成する場合のためにJSONファイルもアップロードされます。それについては後ほど説明します。

これは、スタティックタイプが常に正確かつ最新であることを意味しています。また、プロジェクトが他の静的型付け言語のWorkersを実行して、私たちの中間表現から彼ら独自のタイプを生成することができます。こちらはCloudflareボットからのPR例です。ランタイム型の変更が検出され、中間表現と共にTypeScriptファイルが更新されます。

自動生成された型の使用

最初に、以下のように「wrangler」を使用して新しいTypeScriptプロジェクトを生成します。

$ wrangler generate my-typescript-worker https://github.com/cloudflare/worker-typescript-template

既にTypeScriptプロジェクトがある場合は、以下を使用して最新バージョンのworkers-typesをインストールします。

$ npm install --save-dev @cloudflare/workers-types

次に、 @cloudflare/workers-types をプロジェクトのtsconfig.jsonファイルに追加します。

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "lib": ["ES2020"],
    "types": ["@cloudflare/workers-types"]
  }
}

その後、ユーザーが選択したIDEで型が自動作成されます。

仕組み

以下は、Workersランタイムのコードベースからのサンプルコードです。

class Blob: public js::Object {
public:
  typedef kj::Array<kj::OneOf<kj::Array<const byte>, kj::String, js::Ref<Blob>>> Bits;
  struct Options {
    js::Optional<kj::String> type;
    JS_STRUCT(type);
  };

  static js::Ref<Blob> constructor(js::Optional<Bits> bits, js::Optional<Options> options);
  
  int getSize();
  js::Ref<Blob> slice(js::Optional<int> start, js::Optional<int> end);

  JS_RESOURCE_TYPE(Blob) {
    JS_READONLY_PROPERTY(size, getSize);
    JS_METHOD(slice);
  }
};

Pythonスクリプトはビルドごとにこのコード上で実行され、識別子、あらゆる引数タイプ、あらゆる戻り値のタイプを含む関数に関する情報が記された抽象構文ツリーが生成されます。

{
  "name": "Blob",
  "kind": "class",
  "members": [
    {
      "name": "size",
      "type": {
        "name": "integer"
      },
      "readonly": true
    },
    {
      "name": "slice",
      "type": {
        "params": [
          {
            "name": "start",
            "type": {
              "name": "integer",
              "optional": true
            }
          },
          {
            "name": "end",
            "type": {
              "name": "integer",
              "optional": true
            }
          }
        ],
        "returns": {
          "name": "Blob"
        }
      }
    }
  ]
}

最後に、TypeScript型のリポジトリに、更新された型のPRが自動的に送信されます。

declare type BlobBits = (ArrayBuffer | string | Blob)[];

interface BlobOptions {
  type?: string;
}

declare class Blob {
  constructor(bits?: BlobBits, options?: BlobOptions);
  readonly size: number;
  slice(start?: number, end?: number, type?: string): Blob;
}

オーバーライド

一部のケースで、TypeScriptはC++ランタイムでサポートされない概念をサポートします。つまり、ジェネリックおよび関数のオーバーロードです。これらの場合、生成された型をpartial宣言でオーバーライドします。例えば、DurableObjectStorageではgetterおよびsetter関数のためにジェネリックが多用されます。

declare abstract class DurableObjectStorage {
	 get<T = unknown>(key: string, options?: DurableObjectStorageOperationsGetOptions): Promise<T | undefined>;
	 get<T = unknown>(keys: string[], options?: DurableObjectStorageOperationsGetOptions): Promise<Map<string, T>>;
	 
	 list<T = unknown>(options?: DurableObjectStorageOperationsListOptions): Promise<Map<string, T>>;
	 
	 put<T>(key: string, value: T, options?: DurableObjectStorageOperationsPutOptions): Promise<void>;
	 put<T>(entries: Record<string, T>, options?: DurableObjectStorageOperationsPutOptions): Promise<void>;
	 
	 delete(key: string, options?: DurableObjectStorageOperationsPutOptions): Promise<boolean>;
	 delete(keys: string[], options?: DurableObjectStorageOperationsPutOptions): Promise<number>;
	 
	 transaction<T>(closure: (txn: DurableObjectTransaction) => Promise<T>): Promise<T>;
	}

また、 Markdownを使用して型のオーバーライドを記述することもできます。こちらはKVNamespaceの型をオーバーライドする例です。

独自のタイプの作成

JSON IR (中間表現)はTypeScriptの型と共にオープンソース化され、このGiHubリポジトリ内に置かれています。また、IRの書式を示す、タイプスキーマ自体もオープンソース化しました。自分の言語のWorkersタイプを作成する場合は、「正規化された」データ構造で宣言を記述するIRを取得し、そこから型を生成することができます。

`workers.json`内の宣言には、関数シグネチャーを導出するための要素と、識別子、引数型、戻り値型、エラー管理などの、コード生成に必要な他の要素が含まれています。具体的なユースケースとしては、WebAssemblyをコンパイルする言語のための外部関数宣言を生成して、Workersランタイムから利用可能な一連の関数呼び出しを正確にインポートすることです。

まとめ

CloudflareはTypeScriptおよびRustのエコシステムのサポートに大きな関心を持っています。Brendan氏は両方の言語の型情報を常に最新かつ正確に保つためのツールを作成しました。また、JSONフォーマットの型情報自体をオープンソース化しているため、関心のある方はどなたでもお好みの任意の言語で型データを作成することができます!