本页内容

类型和类型声明

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 在类型检查时使用该文件而不是 JavaScript 文件,使用 @deno-types 编译器提示。@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/pkg@1.0.0/types.d.ts" />

使用配置文件 跳转到标题

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

{
  "compilerOptions": {
    "types": [
      "./types.d.ts",
      "https://deno.land/x/pkg@1.0.0/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 cachedeno 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 不兼容。

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

import React from "https://esm.sh/react?no-dts";

JavaScript 在类型检查时的行为 跳转到标题

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

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

内部机制 跳转到标题

虽然不需要了解 Deno 的内部工作原理才能很好地利用 TypeScript 与 Deno,但了解其工作原理会有所帮助。

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

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

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

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