跳至主要内容

类型和类型声明

Deno 的设计原则之一是禁止非标准模块解析。当 TypeScript 对文件进行类型检查时,它只关心该文件的类型,而 tsc 编译器有许多逻辑来尝试解析这些类型。默认情况下,它期望具有扩展名的模糊模块说明符,并将尝试在 .ts 说明符下查找文件,然后是 .d.ts,最后是 .js(以及当模块解析设置为 "node" 时的一整套其他逻辑)。Deno 处理显式说明符。

不过,这可能会导致一些问题。例如,假设我想使用一个已经编译成 JavaScript 的 TypeScript 文件以及一个类型定义文件。所以我有 `mod.js` 和 `mod.d.ts`。如果我尝试将 `mod.js` 导入 Deno,它只会按照我的要求执行,导入 `mod.js`,但这意味着我的代码不会像 TypeScript 考虑 `mod.d.ts` 文件代替 `mod.js` 文件那样得到很好的类型检查。

为了在 Deno 中支持这一点,Deno 有两种解决方案,其中一种解决方案的变体可以增强支持。你会遇到的两种主要情况是

  • 作为 JavaScript 模块的导入者,我知道应该对模块应用哪些类型。
  • 作为 JavaScript 模块的提供者,我知道应该对模块应用哪些类型。

后一种情况是更好的情况,这意味着作为模块的提供者或主机,每个人都可以使用它,而无需弄清楚如何为 JavaScript 模块解析类型,但当使用可能无法直接控制的模块时,也需要能够执行前者。

导入时提供类型

如果你正在使用一个 JavaScript 模块,并且你已经创建了类型(一个 `.d.ts` 文件)或以其他方式获得了要使用的类型,你可以指示 Deno 在类型检查时使用该文件,而不是使用 `@deno-types` 编译器提示的 JavaScript 文件。`@deno-types` 需要是一个单行双斜杠注释,当使用时会影响下一个导入或重新导出语句。

例如,如果我有一个 JavaScript 模块 `coolLib.js`,并且我有一个单独的 `coolLib.d.ts` 文件想要使用,我会像这样导入它

// @deno-types="./coolLib.d.ts"
import * as coolLib from "./coolLib.js";

在类型检查 `coolLib` 以及你在文件中的使用时,将使用 `coolLib.d.ts` 类型,而不是查看 JavaScript 文件。

编译器提示的模式匹配比较宽松,它将接受指定符的带引号和不带引号的值,以及等号前后出现的空格。

托管时提供类型

如果您控制着模块的源代码,或者您控制着文件在 Web 服务器上的托管方式,那么有两种方法可以告知 Deno 给定模块的类型,而无需导入者执行任何特殊操作。

使用三斜线引用指令

Deno 支持使用三斜线引用 types 指令,该指令采用 TypeScript 文件中 TypeScript 使用的引用注释来包含其他文件,并将其仅应用于 JavaScript 文件。

例如,如果我创建了 coolLib.js,并且在它旁边创建了我的库的类型定义 coolLib.d.ts,我可以在 coolLib.js 文件中执行以下操作

/// <reference types="./coolLib.d.ts" />

// ... the rest of the JavaScript ...

当 Deno 遇到此指令时,它将解析 ./coolLib.d.ts 文件,并在 TypeScript 对文件进行类型检查时使用它,而不是 JavaScript 文件,但在运行程序时仍然加载 JavaScript 文件。

ℹ️ 注意 这是一个为 TypeScript 重新利用的指令,它只适用于 JavaScript 文件。在 TypeScript 文件中使用 types 的三斜线引用指令在 Deno 中也能正常工作,但与 path 指令的行为基本相同。

使用 X-TypeScript-Types 标头

与三斜线指令类似,Deno 支持一个用于远程模块的标头,该标头指示 Deno 在哪里查找给定模块的类型。例如,https://example.com/coolLib.js 的响应可能如下所示

HTTP/1.1 200 OK
Content-Type: application/javascript; charset=UTF-8
Content-Length: 648
X-TypeScript-Types: ./coolLib.d.ts

当看到此标头时,Deno 将尝试检索 https://example.com/coolLib.d.ts,并在类型检查原始模块时使用它。

使用环境或全局类型

总的来说,最好在 Deno 中使用模块/UMD 类型定义,其中模块明确导入它依赖的类型。模块化类型定义可以通过类型定义中的 declare global 表达全局范围的增强。例如

declare global {
var AGlobalString: string;
}

这将使 AGlobalString 在导入类型定义时在全局命名空间中可用。

但在某些情况下,当利用其他现有的类型库时,可能无法利用模块化类型定义。因此,在类型检查程序时,有一些方法可以包含任意类型定义。

使用三斜线指令

此选项将类型定义与代码本身耦合。通过在模块类型附近添加三斜杠 `types` 指令,对文件进行类型检查将包括类型定义。例如

/// <reference types="./types.d.ts" />

提供的说明符与 Deno 中的任何其他说明符一样解析,这意味着它需要扩展名,并且相对于引用它的模块。它也可以是完全限定的 URL

/// <reference types="https://deno.land/x/[email protected]/types.d.ts" />

使用配置文件

另一种选择是使用配置了包含类型定义的配置文件,方法是向 `“compilerOptions”` 提供 `“types”` 值。例如

{
"compilerOptions": {
"types": [
"./types.d.ts",
"https://deno.land/x/[email protected]/types.d.ts",
"/Users/me/pkg/types.d.ts"
]
}
}

与上面的三斜杠引用一样,在 `“types”` 数组中提供的说明符将像 Deno 中的其他说明符一样解析。对于相对说明符,它将相对于配置文件的路径解析。确保通过指定 `--config=path/to/file` 标志告诉 Deno 使用此文件。

类型检查 Web Workers

当 Deno 在 Web Worker 中加载 TypeScript 模块时,它会自动根据 Deno Web Worker 库对模块及其依赖项进行类型检查。这在其他情况下(如 `deno cache` 或编辑器)可能是一个挑战。有几种方法可以指示 Deno 使用 Worker 库而不是标准 Deno 库。

使用三斜杠指令

此选项将库设置与代码本身耦合。通过在 Worker 脚本入口点文件的顶部附近添加以下三斜杠指令,Deno 现在将将其类型检查为 Deno Worker 脚本,而与模块的分析方式无关

/// <reference no-default-lib="true" />
/// <reference lib="deno.worker" />

第一个指令确保不使用其他默认库。如果省略此指令,您将获得一些冲突的类型定义,因为 Deno 将尝试应用标准 Deno 库。第二个指令指示 Deno 应用内置的 Deno Worker 类型定义以及依赖库(如 `“esnext”`)。

当您运行 `deno cache` 或 `deno bundle` 命令或使用使用 Deno 语言服务器的 IDE 时,Deno 应该自动检测这些指令并在类型检查时应用正确的库。

这唯一的缺点是,它使代码在其他非 Deno 平台(如 tsc)上难以移植,因为只有 Deno 内置了 "deno.worker" 库。

使用配置文件

另一个选择是使用一个配置了应用库文件的配置文件。一个可以正常工作的最小文件看起来像这样

{
"compilerOptions": {
"target": "esnext",
"lib": ["deno.worker"]
}
}

然后在命令行上运行命令时,需要传递 --config path/to/file 参数,或者如果您使用的是利用 Deno 语言服务器的 IDE,则设置 deno.config 设置。

如果您还有非工作脚本,则需要省略 --config 参数,或者使用一个配置为满足非工作脚本需求的配置文件。

重要事项

类型声明语义

类型声明文件(.d.ts 文件)遵循与 Deno 中其他文件相同的语义。这意味着声明文件被假定为模块声明(UMD 声明),而不是环境/全局声明。Deno 如何处理环境/全局声明是不可预测的。

此外,如果类型声明导入其他内容,例如另一个 .d.ts 文件,则其解析遵循 Deno 的正常导入规则。对于许多在网上生成和可用的 .d.ts 文件,它们可能与 Deno 不兼容。

为了克服这个问题,一些解决方案提供商,例如 Skypack CDN,会自动捆绑类型声明,就像它们提供 JavaScript 的 ESM 捆绑包一样。

Deno 友好 CDN

有一些 CDN 托管与 Deno 很好地集成的 JavaScript 模块。

  • esm.sh 是一个 CDN,默认情况下提供类型声明(通过 X-TypeScript-Types 标头)。可以通过在导入 URL 后附加 ?no-dts 来禁用它

    import React from "https://esm.sh/react?no-dts";
  • Skypack.dev 是另一个 CDN,它也在您将 ?dts 作为查询字符串附加到远程模块导入语句时提供类型声明(通过 X-TypeScript-Types 标头)。以下是一个示例

    import React from "https://cdn.skypack.dev/react?dts";

JavaScript 在类型检查时的行为

如果您在 Deno 中将 JavaScript 导入 TypeScript,并且没有类型,即使您将 checkJs 设置为 false(Deno 的默认值),TypeScript 编译器仍然会访问 JavaScript 模块并尝试对其进行一些静态分析,至少尝试确定该模块导出的形状以验证 TypeScript 文件中的导入。

在尝试导入“常规”ES 模块时,这通常不会出现问题,但在某些情况下,如果模块具有特殊的打包方式,或者是一个全局的 UMD 模块,TypeScript 对模块的分析可能会失败并导致误导性的错误。在这种情况下,最好的做法是使用上述方法之一提供某种形式的类型。

内部机制

虽然理解 Deno 的内部机制并非充分利用 Deno 中 TypeScript 的必要条件,但它有助于理解 Deno 的工作原理。

在执行或编译任何代码之前,Deno 通过解析根模块生成模块图,然后检测其所有依赖项,并递归地检索和解析这些模块,直到所有依赖项都被检索。

对于每个依赖项,有两个潜在的“槽位”可用。一个是代码槽位,另一个是类型槽位。在填充模块图时,如果模块是或可以被发射到 JavaScript 的东西,它将填充代码槽位,而只有类型依赖项,比如 .d.ts 文件,将填充类型槽位。

当模块图构建完毕,并且需要对图进行类型检查时,Deno 启动 TypeScript 编译器,并向其提供需要被潜在发射为 JavaScript 的模块名称。在此过程中,TypeScript 编译器将请求额外的模块,Deno 将查看依赖项的槽位,如果类型槽位已填充,则先提供类型槽位,然后再提供代码槽位。

这意味着当你导入 .d.ts 模块,或者使用上述解决方案之一为 JavaScript 代码提供替代类型模块时,在解析模块时,TypeScript 将获得这些内容。