订阅以接收新文章的通知:

改善 Workers TypeScript 支持:正确性、人体工程学和互操作性

2022/11/18

9 分钟(阅读时间)
Improving Workers TypeScript support: accuracy, ergonomics and interoperability

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-typesnpm 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.jsontypes 字段中包括 @cloudflare/workers-types。示例如下:

import type { KVNamespace } from "@cloudflare/workers-types";

declare const USERS_NAMESPACE: KVNamespace;

此外,我们还自动生成我们的类型与 TypeScript 的 lib.webworker.d.ts 之间的差异。未来我们还将用此来发现我们可以在哪些方面提高合规性。

与兼容性日期更加兼容

Cloudflare 对我们提供的所有 API 保持强大的向后兼容性承诺。我们使用兼容性标志和日期,以向后兼容的方式进行重大更改。有时这些兼容性标志会更改类型。例如,global_navigator 标志添加了一个新的全局 navigator,而 url_standard 标志更改了 URLSearchParams 构造函数签名。

现在您可以选择与您的兼容性日期相匹配的类型版本,让您可以确保没有使用运行时不支持的功能。

{
  "compilerOptions": {
    ...
    "types": ["@cloudflare/workers-types/2022-08-04"]
  }
}

进一步与 Wrangler 集成

除了兼容性日期外,您的 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 的文档也纳入其中。

docs in types shown with code completions

对于已经使用 Workers 平台的开发人员来说,可能很难看出类型如何随 @cloudflare/workers-types 的每个版本变化。为了避免类型错误,并突出显示新功能,我们现将随每个版本生成一份详细的变更日志,将新的、变更的和删除的定义拆分出来

类型生成是如何在底层实现的?

如前所述,我们已经完全重构了自动类型生成脚本,使其更加可靠、可扩展且可维护。这意味着,发布新的运行时版本后,开发人员便可获得改进后的类型。我们的系统现在使用 workerd 的新版运行时类型信息 (RTTI) 系统来查询 Workers 运行时 API 的类型,而不是试图从解析后的 C++ AST 中提取这些信息。

// 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 = [ ... ], ...)

然后我们将这个 RTTI 传递给 TypeScript 程序,该程序使用 TypeScript 编译器 API 来生成声明并执行 AST 转换进行整理。这已内置在 workerd 的 Bazel 构建系统中,因此,生成类型现在只是一个 bazel build //types:types 命令。我们利用 Bazel 的缓存,在生成过程中尽可能减少重构。

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>; }
new automatic type generation architecture

虽然自动生成的类型正确描述了 Workers 运行时 API 的 JavaScript 接口,但 TypeScript 提供了一些附加功能,我们可以用来提供更高保真度的类型,对开发人员来说更加符合人体工程学。我们的系统允许我们手写部分 TypeScript“覆盖”,与自动生成的类型合并。这让我们能够……

  • ReadableStream 等类型添加类型参数(通用),并避免 any 类型化的值。
  • 用方法过载指定输入和输出类型之间的对应关系。例如,当 type 参数为 text(除 ArrayBuffer 外,如果是 arrayBuffer)时,KVNamespace#get() 应该返回一个 string
  • 重命名类型,以符合 TypeScript 标准并简化语言。
  • 完全替换一个类型,以获得更准确的声明。例如,我们使用 Object.values()WebSocketPairconst 声明替换为更好的类型。
  • 为内部非类型化的值(例如 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 发起讨论

我们保护整个企业网络,帮助客户高效构建互联网规模的应用程序,加速任何网站或互联网应用程序抵御 DDoS 攻击,防止黑客入侵,并能协助您实现 Zero Trust 的过程

从任何设备访问 1.1.1.1,以开始使用我们的免费应用程序,帮助您更快、更安全地访问互联网。要进一步了解我们帮助构建更美好互联网的使命,请从这里开始。如果您正在寻找新的职业方向,请查看我们的空缺职位
Developer Week (CN)Developers (CN)Wrangler (CN)简体中文

在 X 上关注

Brendan Coll|@_mrbbot
Cloudflare|@cloudflare

相关帖子