新特性的初探

考虑以下文件:

// demo.mts
function main(message: string): void {
  console.log('Message: ' + message);
}
main('Hello!');

现在我们可以像这样运行它:

node demo.mts

目前,我们会收到以下警告:

ExperimentalWarning: Type Stripping is an experimental feature and
might change at any time
ExperimentalWarning: 类型剥离是一个实验性功能,
ExperimentalWarning: 随时可能更改

我们可以关闭这个警告:

node --disable-warning=ExperimentalWarning demo.mts

使用 --disable-warning 的技巧:

v23.6.0 版本开始,Node.js 无需任何标志即可支持 TypeScript。这篇博文解释了它是如何工作的以及需要注意什么。

文件扩展名

  • .ts 文件的工作方式类似于 .js 文件,它们可以是 ESM 或 CommonJS。
    • 对于项目中的文件来说,这是一个不错的选择——它们的 package.json 通常包含 "type": "module"
  • .mts 文件始终是 ESM。
    • 对于独立文件,请使用此文件扩展名。
  • .cts 文件始终是 CommonJS。
  • 不支持 .tsx 文件。

Node.js TypeScript 与普通 TypeScript 有何不同?

Node.js 中当前对 TypeScript 的支持是通过 类型剥离 完成的:Node.js 所做的只是删除所有与类型相关的语法。它从不转译任何内容。让我们探讨一下这如何改变我们编写 TypeScript 的方式。

不支持非 JavaScript 语言特性

这些包括:

  • 枚举 (Enums)
  • 命名空间 (Namespaces)
  • 类构造函数中的 参数属性 (Parameter properties)。

不支持 JSX

既不支持 .tsx 文件也不支持 JSX。

不支持编译为当前 JavaScript 的未来 JavaScript 特性

TypeScript 支持一些即将到来的 JavaScript 特性,并将它们转译,以便它们可以在当前的 JavaScript 引擎上运行。其中一个特性是 装饰器 (decorators)。当 JavaScript 支持装饰器时,Node.js 也将在 TypeScript 中支持它们。

本地导入必须引用 TypeScript 文件

在传统的 TypeScript 中,我们引用模块的转译版本:

import { myFunction } from './my-module.js';

为什么会这样?传统上,TypeScript 编译器从不触及模块标识符,例如 './my-module.js'。因此,我们必须使用在转译输出中才有意义的模块标识符。

鉴于 Node.js 使用文件扩展名来确定模块的类型,这种方法必须改变。我们现在必须这样写:

import { myFunction } from './my-module.ts';

我个人甚至对于不打算在 Node.js 上运行的代码也更喜欢这种方法。关于 tsconfig.json 的章节 解释了在这种情况下如何使用它。

类型必须通过类型导入导入

如果我们想导入类型,我们必须使用 类型导入 (type imports)——否则类型剥离将不会删除它们。

// 类型导入
// Type import
import type { Cat, Dog } from './animal.ts';

// 内联类型导入
// Inline type import
import { createCatName, type Cat, type Dog } from './animal.ts';

tsconfig.json

Node 的类型剥离会忽略 tsconfig.json,但如果我们想在开发期间进行类型检查,我们需要一个 tsconfig.json 文件。这是 Node.js 文档推荐的最小配置:

{
  "compilerOptions": {
     "target": "esnext",
     "module": "nodenext",
     "allowImportingTsExtensions": true,
     "rewriteRelativeImportExtensions": true,
     "verbatimModuleSyntax": true
  }
}

对于更大的项目,我们可能想要运行 tsc 并使用名为 noEmitcompilerOption

让我们仔细看看最后三个选项:

  1. allowImportingTsExtensions [TypeScript 5.0 版本开始支持] 允许我们导入 TypeScript 文件(扩展名为 .ts 等),而在传统上我们必须导入它们的转译版本(扩展名为 .js 等)。
  2. rewriteRelativeImportExtensions [TypeScript 5.7 版本开始支持] 将来自 TypeScript 文件(扩展名为 .ts 等)的相对导入转译为来自 JavaScript 文件(扩展名为 .js 等)的相对导入。
  3. verbatimModuleSyntax [TypeScript 5.0 版本开始支持] 如果我们在导入类型时没有使用 type 关键字,则会发出警告。

选项 #1 启用类型检查:没有它,TypeScript 将找不到导入的文件。

选项 #2 启用将 Node.js TypeScript 转译为 JavaScript。

--input-type

--input-type 告诉 Node.js 如何解释代码,当代码不是来自文件时(文件名扩展名包含该信息)——即,当代码来自 stdin 或 --eval 时。此标志现在支持以下值:

  • module
  • commonjs
  • module-typescript
  • commonjs-typescript

类型剥离和 source maps

Ashley Claymore 为 Bloomberg 率先提出的 ts-blank-space 为类型剥离提供了一种巧妙的方法:如果不是简单地删除所有与类型相关的文本,而是用空格“覆盖”它,那么输出中的源代码位置就不会改变,并且堆栈跟踪等仍然正确。因此,不需要 source maps。

例如 - 输入 (TypeScript):

function describeColor(color: Color): string {
  return `Color named “${color.colorName}”`;
}
type Color = { colorName: string };
describeColor({ colorName: 'green' });

输出 (JavaScript):

function describeColor(color       )         {
  return `Color named “${color.colorName}”`;
}

describeColor({ colorName: 'green' });

请注意 describeColor() 的声明及其调用之间的空行。

Node.js 类型剥离使用相同的方法,因此不生成 source maps。

接下来是什么?

TypeScript 5.8 可以警告你关于类型剥离不支持的结构

引用 Jake Bailey (微软 TypeScript 团队的成员) 的话:

一个标志,用于禁止具有运行时输出的 TS 特性(枚举、命名空间、实验性装饰器等)将在 5.8 版本中推出,以帮助人们通过 Node.js 执行 TS 代码(或者那些出于“某些原因”想要避免使用这些特性的人)。

--experimental-transform-types

正在进行中的特性 --experimental-transform-types 实际上将转译 TypeScript,因此将支持更多特性。它将生成 source maps 并默认启用 source maps。

一些随意的想法

  • 我对 TypeScript 的许多文件扩展名感到矛盾(在没有更好的选择的情况下):.ts.tsx.mts.cts,但它们在 Node.js 中确实派上了用场。
  • 感觉类型剥离应该能够直接将修剪后的解析代码馈送到 V8 JavaScript 引擎中。也许这就是浏览器中 TypeScript 支持的未来?支持类型剥离,而不是 JavaScript 的语法扩展。

延伸阅读