Posted in

【第 3359 期】没人告诉你的一件关于 TypeScript 的事_AI阅读总结 — 包阅AI

包阅导读总结

1. `TypeScript`、`tsconfig.json`、`项目引用`、`类型检查`、`配置管理`

2. 本文主要探讨了 TypeScript 中 `tsconfig.json` 配置文件的作用及使用方式,通过示例揭示了一些常见的误解,强调为不同环境创建独立配置以确保类型安全,并指出应深入理解其工作原理以更好地利用 TypeScript。

3.

– 前言

– 介绍了文章主题为 `tsconfig.json` 配置文件的作用和使用方式,以及项目引用对类型检查的管理。

– 正文

– 以简单前端应用为例,解释 TypeScript 通过默认定义库 `lib.dom` 识别 `document`。

– 为应用添加测试框架 `Vitest` 时,通过修改 `tsconfig.json` 启用全局 `it`。

– 指出引用不存在的代码会出现幽灵定义问题,如对 `test` 的错误处理。

– 强调 `include` 选项控制配置影响的模块范围,不应随意扩展,否则会导致类型泄漏。

– 提出应为不同环境创建独立的 `tsconfig.json`,如源文件、测试、构建等,并介绍了根级配置和引用的设置。

– 说明了 `include` 和 `references` 属性的区别,以集成测试配置为例。

– 提及框架可能处理配置,但理解 TypeScript 原理仍有益。

思维导图:

文章地址:https://mp.weixin.qq.com/s/mE68me1IP1G5rYQKQ7yiUg

文章来源:mp.weixin.qq.com

作者:桃猿

发布时间:2024/8/28 0:00

语言:中文

总字数:3892字

预计阅读时间:16分钟

评分:88分

标签:TypeScript,tsconfig.json,项目引用,类型检查,前端开发


以下为原文内容

本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com

前言

主要讲述了 TypeScript 中tsconfig.json配置文件的作用和使用方式,以及如何通过项目引用(Project References)来更好地管理不同环境下的类型检查。今日前端早读课文章由 @桃猿翻译分享,公号:@桃猿授权。

正文从这开始~~

我已经使用 TypeScript 超过四年了,总的来说,这是一段很棒的经历。随着时间的推移,使用它的摩擦力逐渐减少,直到消失,让我在编写类型或以类型为先的方式解决问题时变得更加高效。虽然我远不是一个真正的类型大师,但我敢于认为自己对这门语言很熟练,经历了各种类型体操、条件类型、嵌套泛型,并深入研究了 type 和 interface 之间的神圣区别。说实话,我以为我对这门语言理解得相当好。

【第3344期】换个角度看 TypeScript

直到我发现我错了。看,有一件关于 TypeScript 的特别事情我完全搞错了,我相信你也一样。而且这不是你从未听说过的某个复杂的边缘案例并且可能永远不会使用。恰恰相反,这是你和其他所有 TypeScript 开发者直接交互过数百次的东西,一直在我们眼皮底下游动的东西。

我说的是tsconfig.json

不,这不是关于它有多复杂(我承认我不能不假思索地解释 target 和 module)。相反,这是一件非常简单的事情。它是关于tsconfig.json实际做了什么。

“嗯,它是一个配置文件,它配置 TypeScript,废话。” 对!它是,但不是你期望的方式。让我给你看。

库、测试和真相

每一个伟大发现的背后都有一个伟大的例子。我会尽力让这既伟大又简单。

让我们写一个简单的前端应用程序。我是认真的,没有框架,没有依赖。简单。

 // src/app.ts
const greetingText = document.createElement("p")
greetingText.innerText = "Hello, John!"

document.body.appendChild(greetingText)

创建一个段落元素并向 John 问好。简单。到目前为止一切都很好。

但是 document 从哪里来的?你可以说它是 JavaScript 中的全局变量,你说得对。只有一件事。我们还不是在 JavaScript 中。还没有,真的。我们在 IDE 中查看一些 TypeScript 代码。它必须被编译成 JavaScript,才能进入浏览器,并让浏览器向它公开 document 全局变量。那么 TypeScript 如何知道 document 的存在及其方法呢?

TypeScript 通过加载一个默认的 定义库 叫做 lib.dom 来做到这一点。把它想象成一个包含一堆类型的.d.ts文件,用来描述 JavaScript 全局变量,因为它确实是这样的。你可以通过按住 CMD(在 Windows 上是 CTRL)并点击 document 对象来亲自看看。谜团解决了。

【早阅】Typescript:类型细化中的“as const”

因为我们的应用程序自然是自切面包以来最好的东西,让我们为它添加一些自动化测试。为此,我们将背叛我们对简单性的概念,安装一个叫做 Vitest 的测试框架。接下来,我们编写测试本身:

 // src/app.test.ts
it("greets John", async () => {
await import("./app")
const greetingText = document.querySelector("p")
expect(greetingText).toHaveText("Hello, John!")
})

一旦我们尝试运行这个测试,TypeScript 就会 干扰 并报错:

 Cannot find name 'it'. Do you need to install type definitions for a test runner?

确认这一点让我很伤心,但编译器是对的。it 从哪里来呢?它不是像 document 那样的全局变量,它必须来自某个地方。实际上,测试框架很常见地会扩展全局对象,并全局暴露 it 和 expect 这样的函数,这样你就可以在每个测试中访问它们而不需要显式导入。

我们按照测试框架文档中方便存在的部分,通过修改tsconfig.json启用全局 it:

 // tsconfig.json
{
"compilerOptions": {
"types": ["vitest/globals"]
},
"include": ["src"]
}

通过使用compilerOptions.types,我们要求 TypeScript 加载额外的类型,在这种情况下是从vitest/globals加载,声明全局的 it 函数。编译器对我们的努力露出微笑,并让测试通过,这让我们对自己和整个严格类型语言的过程感到特别满意。

但是,这事儿没有这么快。

问题

我们借一步说话,但我保证最终这一切都会有意义。

让我问你这个问题:如果你在 TypeScript 中引用了一个不存在的代码会发生什么?是的,一条波浪形的红线和 Cannot find name 类型错误,这就是会发生的事情。刚刚我们在测试中尝试调用it()时已经见过了。

跳回到app.ts模块,并添加一个对不存在的全局变量 test 的引用:

 // src/app.ts
// ...application code.

test

我们没有定义 test。它不是一个浏览器全局变量,当然也不存在于任何 TypeScript 默认库中。这是一个错误,一个 bug,它必须显示为红色。

只是,事实并非如此。因为波浪形的红线并没有出现在代码下方,一股力量流经你身。权威。困惑。更糟糕的是,不仅 TypeScript 在这里没有产生错误,它实际上试图帮忙,建议我们 定义 test,显示它的调用签名,说它来自某个 TestApi 命名空间。但那是 Vitest 的类型,这怎么可能……

这个代码会编译吗?当然。它会在浏览器中工作吗?不会。它会像一个投球手在最辉煌的那天一样,老练的抛出错误。为什么会这样?难道使用 TypeScript 的全部目的是为了防止这样的错误吗?

这里的 test 是我所称的 幽灵定义_。它是一个有效的类型定义,描述了一些根本不存在的东西。又一个 TypeScript 的诡计, 你说。不要急着责怪工具,我说。这是这么回事。

(超越)一个配置统治一切

app.test.ts测试模块从 src 目录移动到新创建的 test 目录中。打开它。等等,it 上又有类型错误吗?我们不是已经通过在tsconfig.json中添加vitest/globals解决了吗?

问题在于,TypeScript 不知道如何处理 test 目录。实际上,TypeScript 甚至不知道它的存在,因为我们在tsconfig.json中指向的只有 src:

 // tsconfig.json
{
"compilerOptions": {
"types": ["vitest/globals"]
},
"include": ["src"]
}

正如我之前提到的,TypeScript 配置的工作方式并不是完全显而易见的。很长一段时间我都认为 include 选项是用于包含在编译中的模块,而 exclude 则分别控制哪些模块被排除。如果我们查阅 TypeScript 文档,我们会读到:

include 指定一个文件名或模式数组,以包含在程序中。

我对 include 的理解稍有不同,比文档中所述更具体。

include 选项控制应用此 TypeScript 配置影响到哪些模块。

你没看错。如果一个 TypeScript 模块位于 include 选项列出的目录之外,那么tsconfig.json对该模块将完全无效。相应地,exclude 选项允许过滤出不受当前配置影响的文件模式。

好了,所以我们将 test 添加到 include 中,然后继续我们的一天,有什么大不了的?

 // tsconfig.json
{
"compilerOptions": {
"types": ["vitest/globals"]
},
"include": ["src", "test"]
}

这是大多数开发者完全错误的地方。通过在 include 中添加新目录,你正在扩展此配置以影响所有这些目录。虽然此更改修复了 test 中的测试框架类型,但它会将这些类型泄漏到所有 src 模块中!你刚刚让你的整个源代码成为一个闹鬼的豪宅,释放了数百个幽灵类型。不存在的东西将被类型化,已被类型化的东西可能与其他定义冲突,使用 TypeScript 的整体体验将显著下降,特别是随着应用程序的增长。

那么,解决方案是什么呢?我们应该为每个目录创建一堆tsconfig.json吗?

实际上,是的,你应该这样做。只是,不是为每个目录,而是为你的代码运行的每个环境。

运行时和关注点

现代 Web 应用程序的幕后是一个精美的模块沙拉。你的应用程序的即时源码需要被编译、压缩、代码分割、打包并传送给用户。然后是测试文件,这些也是 TypeScript 模块,永远不需要编译或传送给任何人。可能还有 Storybook 故事、Playwright 测试,也许还有一两个自定义的*.ts脚本来自动化任务 —— 所有这些都很有用,都有不同的意图,并且在 不同的环境 中运行。

但我们编写模块 的目的 很重要。这对 TypeScript 也很重要。为什么你认为它默认给你提供 Document 类型?因为它知道你可能在开发一个 Web 应用程序。如果在开发 Node.js 服务器呢?请明确说明这个意图并安装@types/node。编译器不能为你猜测,你需要告诉它你想要什么。

你通过tsconfig.json传达这个意图。但不仅仅是根级别的配置。TypeScript 可以很好地处理嵌套的配置。因为它被设计成那样。你所需要做的就是明确你的意图。

 # The root-level configuration to apply TypeScript

# across the entire project. This mostly contains only references.

- tsconfig.json

# The base configuration that all the other configurations

# extend upon. Describe the shared options here.

- tsconfig.base.json

# The source files configuration.

- tsconfig.src.json

# The build configuration.

- tsconfig.build.json

# Configuratin for integration tests.

- tsconfig.test.json

# Configuration for end-to-end tests.

- tsconfig.e2e.test.json

哇,有很多配置!那么,也有很多意图:从源文件到各个测试级别再到生产构建。所有这些都是为了类型安全。你通过使用 TypeScript 配置的references属性来使它们类型安全!

魔法从根级别的tsconfig.json开始。请放心,这是 TypeScript 唯一会拾取的配置。所有其他配置都成为根级配置的引用,仅适用于匹配其 include 的文件。

这是根级tsconfig.json的样子:

 // tsconfig.json
{
"references": [
// Source files (e.g. everything under "./src").
{ "path": "./tsconfig.src.json" },
// Integration tests (e.g. everything under "./tests").
{ "path": "./tsconfig.test.json" },
// E2E tests (e.g. everything under "./e2e").
{ "path": "./tsconfig.e2e.test.json" }
]
}

既然你使用了 references 字段,所有引用的配置都必须将compilerOptions.composite设置为 true。以下是用于源文件的tsconfig.src.json示例:

 // tsconfig.src.json
{
// Inherit the reused options.
"extends": "./tsconfig.json",
// Apply this configuration only to the files
// under the "./src" directory.
"include": ["./src"],
"compilerOptions": {
"composite": true,
"target": "es2015",
"module": "esnext",
// Support JSX for React applications.
"jsx": "react"
}
}

你为源文件和构建使用单独的配置,因为具有compilerOptions.composite的配置不能直接运行。你将 tsc 指向特定的-p tsconfig.build.json进行构建。

对于交叉的配置会有点复杂,比如集成测试的配置,它应该只适用于./tests下的文件,同时仍然允许你导入被测试的源代码。为此,你再次利用 references 属性!

 // tsconfig.test.json
{
"extends": "./tsconfig.json",
"include": ["./tests"],
"references": [{ "path": "./tsconfig.src.json" }],
"compilerOptions": {
"composite": true,
"target": "esnext",
"module": "esnext",
// Include test-specific types.
"types": ["@types/node", "vitest/globals"]
}
}

references 属性告诉 TypeScript 在类型检查中,依赖给定的配置,但是这个依赖关系不会改变当前配置影响的模块范围。

include vs references

include 和 references 属性都涉及 TypeScript 可见的文件,但它们的方式不同。让我们回顾一下这个区别。

集成测试配置(tsconfig.test.json)完美地说明了这一点。你希望该配置仅适用于./tests目录下的测试文件,所以你在 include 中提供了它。但你也希望能够在这些文件中导入被测试的源代码,这意味着 TypeScript 必须知道这些代码。你在 references 中引用源文件的配置(tsconfig.src.json),这从传递上扩展了 TypeScript 的视野到那里包含的文件,而不受集成测试配置的影响。

实践方面

不管好坏,我们正迈向开发者工具从我们手中抽象化的时代。可以合理地期望你选择的框架为你处理这些配置的丛林。事实上,一些框架已经在这样做了。以 Vite 为例。我相当有信心,你可以在几乎任何其他项目中找到一个 TypeScript 的多配置设置。

但我希望你明白,无论是否抽象化,TypeScript 仍然是你的工具,通过学习更多、更加理解它并正确使用它,你会受益匪浅。

关于本文
译者:@桃猿

译文:https://mp.weixin.qq.com/s/t0ZaUkFWG7il4NQMX0RicA
作者:@Artem

原文:https://kettanaito.com/blog/one-thing-nobody-explained-to-you-about-typescript

这期前端早读课
对你有帮助,帮”
“一下,
期待下一期,帮”
在看” 一下 。