TypeScript 可在程序运行前发现一些类型错误,让开发人员能够轻松编写出不会崩溃的代码。我们希望开发人员充分利用这一工具,因此,一年前我们构建了一个系统,在Cloudflare Workers 运行时自动生成 TypeScript 类型。这让开发人员能够在其 IDE 中观察到 Workers API 的代码完成情况,并在部署之前检查代码类型。每周都会发布新的类型版本,反映最近的变更。
过去一年来,我们从客户和内部团队收到了很多关于如何改进这些类型的反馈。随着切换到 Bazel 构建系统,准备开放运行时源代码,我们看到了重构类型的机会,使类型更准确、更易用、更容易生成。今天,我们隆重宣布推出 @cloudflare/workers-types
的下一个主要版本,其中包括许多新的功能,并开放了完全重写的自动生成脚本的源代码。
如何在 Workers 中使用 TypeScript
在 Workers 中设置 TypeScript 非常简单!如果您才刚开始使用 Workers,请安装 Node.js,然后在终端运行 npx wrangler init
,以生成新项目。如果您有一个现有的 Workers 项目,并希望利用我们改进的类型,请安装最新版本的 TypeScript 和 @cloudflare/workers-types
与 npm install --save-dev typescript
@cloudflare/workers-types@latest
,然后创建一个 tsconfig.json
文件,文件内容如下:
您的编辑器现在会突出显示问题,并在您输入时补全代码,从而减少错误,提升开发人员的体验。
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"lib": ["esnext"],
"types": ["@cloudflare/workers-types"]
}
}
编辑器会突出显示 set
(而不是 put
)使用不正确的地方,并补全代码
使用标准类型提高互操作性
Cloudflare Workers 实施了许多与浏览器相同的运行时 API,我们还在使用 WinterCG 进一步提高我们标准的合规性。然而,浏览器和 Workers 能做的事情始终存在根本区别。例如,浏览器可以播放音频文件,而 Workers 可以直接访问 Cloudflare 的网络,以存储分布在全球的数据。这种不匹配意味着,每个平台提供的运行时 API 和类型并不相同,这反过来又使 Workers 类型难以与在 Cloudflare 网络和浏览器中运行相同文件的框架(例如 Remix)一起使用。这些文件需要对照 lib.dom.d.ts
(与我们的类型不兼容)检查类型。
为了解决这一问题,我们现在生成了一个单独的类型版本,可以选择性导入,而不必在 tsconfig.json
的 types
字段中包括 @cloudflare/workers-types
。示例如下:
此外,我们还自动生成我们的类型与 TypeScript 的 lib.webworker.d.ts
之间的差异。未来我们还将用此来发现我们可以在哪些方面提高合规性。
import type { KVNamespace } from "@cloudflare/workers-types";
declare const USERS_NAMESPACE: KVNamespace;
与兼容性日期更加兼容
Cloudflare 对我们提供的所有 API 保持强大的向后兼容性承诺。我们使用兼容性标志和日期,以向后兼容的方式进行重大更改。有时这些兼容性标志会更改类型。例如,global_navigator
标志添加了一个新的全局 navigator
,而 url_standard
标志更改了 URLSearchParams
构造函数签名。
现在您可以选择与您的兼容性日期相匹配的类型版本,让您可以确保没有使用运行时不支持的功能。
进一步与 Wrangler 集成
{
"compilerOptions": {
...
"types": ["@cloudflare/workers-types/2022-08-04"]
}
}
除了兼容性日期外,您的 Worker 环境配置还会影响运行时和类型 API 表面。如果您在 wrangler.toml
中配置了 KV 命名空间或 R2 存储桶等绑定,它们需要反映在 TypeScript 类型中。同样,需要声明自定义文本、数据和 WebAssembly 模块规则,让 TypeScript 知道导出的类型。以前则是由您来创建一个包含这些声明的单独环境 TypeScript 文件。
为了保持将 wrangler.toml
作为唯一的真实来源,您现可运行 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
的每个版本变化。为了避免类型错误,并突出显示新功能,我们现将随每个版本生成一份详细的变更日志,将新的、变更的和删除的定义拆分出来。
类型生成是如何在底层实现的?
如前所述,我们已经完全重构了自动类型生成脚本,使其更加可靠、可扩展且可维护。这意味着,发布新的运行时版本后,开发人员便可获得改进后的类型。我们的系统现在使用 workerd
的新版运行时类型信息 (RTTI) 系统来查询 Workers 运行时 API 的类型,而不是试图从解析后的 C++ AST 中提取这些信息。
然后我们将这个 RTTI 传递给 TypeScript 程序,该程序使用 TypeScript 编译器 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
等类型添加类型参数(通用),并避免any
类型化的值。用方法过载指定输入和输出类型之间的对应关系。例如,当
type
参数为text
(除ArrayBuffer
外,如果是arrayBuffer
)时,KVNamespace#get()
应该返回一个string
。重命名类型,以符合 TypeScript 标准并简化语言。
完全替换一个类型,以获得更准确的声明。例如,我们使用
Object.values()
将WebSocketPair
与const
声明替换为更好的类型。为内部非类型化的值(例如
Request#cf
对象)提供类型。Workers 中隐藏的无法使用的内部类型。
以前,这些覆盖是在单独的 TypeScript 文件中定义的,而非它们所覆盖的 C++ 声明文件。因此,它们经常与原始声明不同步。在新的系统中,覆盖与含 C++ 宏的原始文件一同定义,这意味着它们可以与运行时实施变更一同接受审查。请参阅 README,了解 workerd
的 JavaScript 粘合代码的更多细节和示例。
立即尝试使用 Workers 生成类型!
我们鼓励您使用 npm install --save-dev @cloudflare/workers-types@latest
升级到 @cloudflare/workers-types
的最新版本,并试用新的 wrangler types
命令。我们将随每个 workerd
版本发布一个新的类型版本。如果您对 Cloudflare Developers Discord 有任何看法,请告知我们;如果您认为有任何类型可以改进,请在 GitHub 发起讨论。