根据过往情况来看,让 Rust 和 TypeScript 类型的存储库保持最新十分困难。它们是手动生成,这意味着面临不准确和过时的风险。直到最近,每当类型发生变化时,都需要手动更新 workers-types 存储库。我们还习惯于为大多数完整的浏览器 API 添加类型信息。这会导致混淆,当人们尝试使用不受 Workers 运行时支持的浏览器 API 时,它们会编译,但会抛出错误。
而在这个夏天,一切都发生了变化。我们的实习生 Brendan Coll 构建了一个可生成类型的自动化管道。每当我们构建 Workers 运行时时,它就会运行一次,为我们的 TypeScript 和 Rust 存储库生成类型。现在,所有内容都准确且为最新。
快速概览
每次构建 Workers 运行时代码时,都会有一个脚本在公共 API 上运行,生成 Rust 和 TypeScript 类型,以及一个包含静态类型中间表示的 JSON 文件。这些类型将被发送到适当的存储库,JSON 文件也会被上传,以防人们想要创建他们自己的类型包。这一点将在后面进行详述。
这意味着静态类型将始终准确且为最新。它还允许项目以其他静态类型语言运行 Workers,以从我们的中间表示生成他们自己的类型。下面是来自我们 Cloudflare 机器人的示例 PR。它在运行时类型中检测到一个变化,并正在更新 TypeScript 文件和中间表示。
使用自动生成的类型
首先,使用 wrangler 生成一个新的 TypeScript 项目:
如果您已经拥有 TypeScript 项目,则可以使用以下命令安装最新版本的 workers-types:
$ wrangler generate my-typescript-worker https://github.com/cloudflare/worker-typescript-template
然后将 @cloudflare/workers-types
添加到项目的 tsconfig.json 文件。
$ npm install --save-dev @cloudflare/workers-types
之后,应当会在您选择的 IDE 中自动完成类型。
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020"],
"types": ["@cloudflare/workers-types"]
}
}
了解详情
下面是来自 Workers 运行时代码库的一些示例代码。
每次构建期间,都会有一个 Python 脚本在该代码上运行,并生成一个 Abstract Syntax Tree,其中包含有关函数的信息,包括标识符、任何参数类型和任何返回类型。
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);
}
};
最后,会向 TypeScript 类型存储库自动传送带有已更新类型的 PR。
{
"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"
}
}
}
]
}
覆盖
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++ 运行时不支持的概念,也就是泛型和函数重载。在这些情况下,我们会使用分部声明覆盖生成的类型。例如,DurableObjectStorage 为其 getter 和 setter 函数使用大量泛型。
您还可以使用 Markdown 编写类型覆盖。这里是覆盖 KVNamespace 类型的一个示例。
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>;
}
创建您自己的类型
JSON IR(中间表示)已经和 TypeScript 类型一起开放源码,可在本 GitHub 存储库中找到。我们还开放了类型架构本身的源码,该类型架构描述了 IR 的格式。如果您有兴趣为您自己的语言生成 Workers 类型,则可以使用 IR,它采用“规范化的”数据结构描述声明,并从中生成类型。
“workers.json”内的声明包含用于派生函数签名的元素和代码生成所需的其他元素,例如标识符、参数类型、返回类型和错误管理。一个具体用例是为编译到 WebAssembly 的语言生成外部函数声明,以精确地从 Workers 运行时导入可用的函数调用集。
总结
Cloudflare 深切关注支持 TypeScript 和 Rust 生态系统。Brendan 制作了一个工具,可确保两种语言的类型信息都始终为最新且准确。我们还以 JSON 格式开源了类型信息本身,这样,感兴趣的任何人都可以为他们想要的任何语言创建类型数据!