从 JavaScript 生成 TypeScript 定义文件

2025-06-07

从 JavaScript 生成 TypeScript 定义文件

open-wc,我们非常推崇无构建开发设置。我们对此有过一两篇文😄。我们相信,未来的关键在于回归 Web 平台。这意味着我们将优先依赖浏览器原生功能,而非用户空间、JavaScript 解决方案或开发工具。正因如此,我们致力于在传统浏览器最终被淘汰之前,为开发者提供使用该平台的工具技术

这种方法在DX、性能和可访问性方面赋予了我们巨大的优势,但也存在一些缺点。众所周知,JavaScript 是动态类型的。想要在开发时享受类型检查的开发者通常会选择微软的 TypeScript、Facebook 的 Flow 或谷歌的 Clojure 编译器。所有这些都需要构建步骤。

我们能否在“忠于” Web 平台的同时,享受安全类型的开发体验?让我们先深入研究一下 Types 能给我们带来什么。

TypeScript 中的示例

假设我们想要一个接受数字或字符串并返回平方值的函数。

// helpers.test.ts
import { square } from '../helpers';

expect(square(2)).to.equal(4);
expect(square('two')).to.equal(4);
Enter fullscreen mode Exit fullscreen mode

我们的函数的 TypeScript 实现可能如下所示:

// helpers.ts
export function square(number: number) {
  return number * number;
}
Enter fullscreen mode Exit fullscreen mode

我知道你在想什么:用字符串作为参数?在实现的过程中,我们发现这也是一个坏主意。

由于 TypeScript 的类型安全性,以及围绕它的成熟的开发工具生态系统(如 IDE 支持),我们甚至可以在运行测试之前就判断哪些测试square('two')行不通。

Microsoft Visual Studio Code 编辑器中 helpers.test.ts 源代码的屏幕截图,清楚地显示第 3 行的错误信号,其中使用字符串作为参数调用函数 square

tsc如果我们在文件上运行 TypeScript 编译器,我们将看到相同的错误:

$ npm i -D typescript
$ npx tsc
helpers.tests.ts:8:19 - error TS2345: Argument of type '"two"' is not assignable to parameter of type 'number'.

8     expect(square('two')).to.equal(4);
                    ~~~~~

Found 1 error.
Enter fullscreen mode Exit fullscreen mode

类型安全帮助我们在将其发布到生产环境之前捕获了这个错误。如何在不使用 TypeScript 作为构建步骤的情况下实现这种类型安全?

在 Vanilla JavaScript 中实现类型安全

我们的第一步是将文件从 重命名为.ts.js然后,我们将在 JavaScript 文件中使用浏览器友好的 import 语句,.js方法是使用带有文件扩展名的相对 URL:

// helpers.test.js
import { square } from '../helpers.js';

expect(square(2)).to.equal(4);
expect(square('two')).to.equal(4);
Enter fullscreen mode Exit fullscreen mode

然后,我们将通过删除显式类型检查将 TypeScript 函数重构为 JavaScript:

// helpers.js
export function square(number) {
  return number * number;
}
Enter fullscreen mode Exit fullscreen mode

现在,如果我们回到我们的测试文件,square('two')当我们将错误的类型(字符串)传递给函数时,我们将不再看到错误😭!

在 JavaScript 版本的测试文件中,当使用字符串调用 string 时,Visual Studio Code 不再显示第 3 行的错误

如果您认为“哦,JavaScript 是动态类型的,对此没有什么可以做的”,那么请查看这一点:我们实际上可以使用 JSDoc 注释在原始 JavaScript 中实现类型安全。

使用 JSDoc 向 JavaScript 添加类型

JSDoc是一种长期存在的 JavaScript 内联文档格式。通常,您可能会使用它来自动生成服务器 API 或Web 组件属性的文档。今天,我们将使用它来实现编辑器中的类型安全。

首先,为你的函数添加 JSDoc 注释。VSCode和Atom的 docblockr 插件可以帮助你快速完成此操作

/**
 * The square of a number
 * @param {number} number
 * @return {number}
 */
export function square(number) {
  return number * number;
}
Enter fullscreen mode Exit fullscreen mode

tsconfig.json接下来,我们将通过向项目的根目录添加来配置 TypeScript 编译器来检查 JavaScript 文件和 TypeScript 文件。

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": ["es2017", "dom"],
    "allowJs": true,
    "checkJs": true,
    "noEmit": true,
    "strict": false,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "types": ["mocha"],
    "esModuleInterop": true
  },
  "include": ["test", "src"]
}
Enter fullscreen mode Exit fullscreen mode

嘿!你不是说我们这里不会用 TypeScript 吗?!

你说得对,虽然我们将编写和发布浏览器标准的 JavaScript,但我们的编辑器工具将在底层使用TypeScript 语言服务器
来提供类型检查。 这样做可以让我们在 VSCode 和 Atom 中获得与 TypeScript 完全相同的行为。

VSCode 的屏幕截图显示了与第一张图相同的类型检查,但使用了带注释的 JavaScript 文件

我们甚至在运行时也会出现相同的行为tsc

$ npx tsc
test/helpers.tests.js:8:19 - error TS2345: Argument of type '"two"' is not assignable to parameter of type 'number'.

8     expect(square('two')).to.equal(4);
                    ~~~~~

Found 1 error.
Enter fullscreen mode Exit fullscreen mode

重构

太好了,我们已经编写了square功能,包括类型检查,并将其推送到生产环境。但过了一段时间,产品团队来找我们,说一个重要的客户希望能够在我们应用这项功能之前,能够为他们增加我们平方后的数字。这次,产品团队已经与 QA 进行了沟通,他们连夜加班,为我们重构的功能提供了以下测试:

expect(square(2, 10)).to.equal(14);
expect(square(2, 'ten')).to.equal(14);
Enter fullscreen mode Exit fullscreen mode

然而,看起来他们可能应该把这些时间用来睡觉,因为我们原来的类型转换错误仍然存​​在。

我们如何才能快速地向客户提供这一关键(😉)功能,同时仍然保持类型安全?

如果我们在 TypeScript 中实现了该功能,您可能会惊讶地发现我们不需要向第二个参数添加显式类型注释,因为我们将为其提供默认值。

export function square(number: number, offset = 0) {
  return number * number + offset;
}
Enter fullscreen mode Exit fullscreen mode

提供的默认值让 TypeScript 静态分析代码来推断值的类型。

我们可以使用 vanilla-js-and-jsdoc 生产实现获得相同的效果:

/**
 * The square of a number
 * @param {number} number
 * @return {number}
 */
export function square(number, offset = 0) {
  return number * number + offset;
}
Enter fullscreen mode Exit fullscreen mode

在这两种情况下,tsc都会出现错误:

test/helpers.tests.js:13:22 - error TS2345: Argument of type '"ten"' is not assignable to parameter of type 'number'.

13     expect(square(2, 'ten')).to.equal(14);
                        ~~~~~
Enter fullscreen mode Exit fullscreen mode

而且在这两种情况下,我们唯一需要添加的offset = 0是它本身就包含类型信息。如果我们想添加显式类型定义,可以添加第二个@param {number} offset注解,但就我们的目的而言,这没有必要。

发布库

如果您希望人们能够使用您的代码,那么您需要在某个时候发布它。对于 JavaScript 和 TypeScript 来说,这通常意味着npm
您还需要为用户提供您一直享受的编辑器级类型安全性。
为此,您可以*.d.ts在要发布的包的根目录中发布类型声明文件 ( )。只要在项目文件夹中找到这些声明文件,TypeScript 和 TypeScript 语言服务器就会默认遵循它们node_modules

对于 TypeScript 文件,这很简单,我们只需将这些选项添加到tsconfig.json...

"noEmit": false,
"declaration": true,
Enter fullscreen mode Exit fullscreen mode

...TypeScript 将为我们生成文件*.js*.d.ts

// helpers.d.ts
export declare function square(number: number, offset?: number): number;

// helpers.js
export function square(number, offset = 0) {
  return number * number + offset;
}
Enter fullscreen mode Exit fullscreen mode

(请注意,该文件的输出js与我们在 js 版本中编写的完全相同。)

发布 JavaScript 库

遗憾的是,目前tsc还不支持*.d.ts从带 JSDoc 注释的文件生成文件。
我们希望将来能够实现,事实上,该功能的原始问题仍然有效,而且似乎已经支持3.7。不要轻信我们的话,拉取请求正在提交中。

事实上,这种方法效果很好,我们在open-wc的生产中使用它。

警告!
这是一个不受支持的版本 => 如果某些功能无法正常工作,则无人会修复。
因此,如果您的用例不受支持,则需要等待 TypeScript 正式发布来支持它。

我们擅自发布了一个分叉版本typescript-temporary-fork-for-jsdoc,它只是上述拉取请求的副本。

为 JSDoc 注释的 JavaScript 生成 TypeScript 定义文件

现在我们已经掌握了所有信息。让我们开始行动吧💪!

  1. 用 JS 编写代码并在需要的地方应用 JSDoc
  2. 使用分叉的 TypeScriptnpm i -D typescript-temporary-fork-for-jsdoc
  3. tsconfig.json至少具备以下条件:

    "allowJs": true,
    "checkJs": true,
    
  4. 通过 进行“类型检查” tsc,最好通过huskypre-commit进行钩子操作

  5. tsconfig.build.json至少

    "noEmit": false,
    "declaration": true,
    "allowJs": true,
    "checkJs": true,
    "emitDeclarationOnly": true,
    
  6. 生成类型tsc -p tsconfig.build.types.json,最好在CI中

  7. 发布您的.js.d.ts文件

我们在open-wc上就有这样的设置,到目前为止它运行良好。

恭喜你现在无需构建步骤即可获得类型安全🎉

您也可以随意查看此帖子的存储库并执行npm run build:typesnpm run lint:types现场观看魔术。

结论

总而言之 - 为什么我们喜欢 TypeScript,即使它需要构建步骤?

归根结底有两点:

  • 类型对于您和/或您的用户来说非常有用(类型安全、自动完成、文档等)
  • TypeScript 非常灵活,并且支持“仅” JavaScript 的类型

更多资源

如果您想了解有关使用 JSDoc 实现类型安全的更多信息,我们推荐以下博客文章:

致谢

在Twitter上关注我们,或者关注我的个人Twitter。请务必访问open-wc.org
查看我们的其他工具和推荐

感谢BennyLarsPascal 的反馈,帮助我将我的涂鸦变成一个可理解的故事。

文章来源:https://dev.to/open-wc/generating-typescript-definition-files-from-javascript-5bp2
PREV
介绍:自定义元素清单
NEXT
宣布开放 Web 组件 宣布开放-wc