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

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

2022-11-18

4 分钟阅读时间
这篇博文也有 EnglishFrançaisDeutsch日本語PortuguêsEspañol繁體中文版本。

TypeScript 可在程序运行前发现一些类型错误,让开发人员能够轻松编写出不会崩溃的代码。我们希望开发人员充分利用这一工具,因此,一年前我们构建了一个系统,在Cloudflare Workers 运行时自动生成 TypeScript 类型。这让开发人员能够在其 IDE 中观察到 Workers API 的代码完成情况,并在部署之前检查代码类型。每周都会发布新的类型版本,反映最近的变更。

Improving Workers TypeScript support: accuracy, ergonomics and interoperability

过去一年来,我们从客户和内部团队收到了很多关于如何改进这些类型的反馈。随着切换到 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。示例如下:

此外,我们还自动生成我们的类型与 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 的每个版本变化。为了避免类型错误,并突出显示新功能,我们现将随每个版本生成一份详细的变更日志,将新的、变更的和删除的定义拆分出来

docs in types shown with code completions

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

如前所述,我们已经完全重构了自动类型生成脚本,使其更加可靠、可扩展且可维护。这意味着,发布新的运行时版本后,开发人员便可获得改进后的类型。我们的系统现在使用 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>; }
new automatic type generation architecture
  • 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开发人员Wrangler

在 X 上关注

Brendan Coll|@_mrbbot
Cloudflare|@cloudflare

相关帖子