从 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);
我们的函数的 TypeScript 实现可能如下所示:
// helpers.ts
export function square(number: number) {
return number * number;
}
我知道你在想什么:用字符串作为参数?在实现的过程中,我们发现这也是一个坏主意。
由于 TypeScript 的类型安全性,以及围绕它的成熟的开发工具生态系统(如 IDE 支持),我们甚至可以在运行测试之前就判断哪些测试square('two')
行不通。
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.
类型安全帮助我们在将其发布到生产环境之前捕获了这个错误。如何在不使用 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);
然后,我们将通过删除显式类型检查将 TypeScript 函数重构为 JavaScript:
// helpers.js
export function square(number) {
return number * number;
}
现在,如果我们回到我们的测试文件,square('two')
当我们将错误的类型(字符串)传递给函数时,我们将不再看到错误😭!
如果您认为“哦,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;
}
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"]
}
嘿!你不是说我们这里不会用 TypeScript 吗?!
你说得对,虽然我们将编写和发布浏览器标准的 JavaScript,但我们的编辑器工具将在底层使用TypeScript 语言服务器
来提供类型检查。 这样做可以让我们在 VSCode 和 Atom 中获得与 TypeScript 完全相同的行为。
我们甚至在运行时也会出现相同的行为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.
重构
太好了,我们已经编写了square
功能,包括类型检查,并将其推送到生产环境。但过了一段时间,产品团队来找我们,说一个重要的客户希望能够在我们应用这项功能之前,能够为他们增加我们平方后的数字。这次,产品团队已经与 QA 进行了沟通,他们连夜加班,为我们重构的功能提供了以下测试:
expect(square(2, 10)).to.equal(14);
expect(square(2, 'ten')).to.equal(14);
然而,看起来他们可能应该把这些时间用来睡觉,因为我们原来的类型转换错误仍然存在。
我们如何才能快速地向客户提供这一关键(😉)功能,同时仍然保持类型安全?
如果我们在 TypeScript 中实现了该功能,您可能会惊讶地发现我们不需要向第二个参数添加显式类型注释,因为我们将为其提供默认值。
export function square(number: number, offset = 0) {
return number * number + offset;
}
提供的默认值让 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;
}
在这两种情况下,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);
~~~~~
而且在这两种情况下,我们唯一需要添加的offset = 0
是它本身就包含类型信息。如果我们想添加显式类型定义,可以添加第二个@param {number} offset
注解,但就我们的目的而言,这没有必要。
发布库
如果您希望人们能够使用您的代码,那么您需要在某个时候发布它。对于 JavaScript 和 TypeScript 来说,这通常意味着npm
。
您还需要为用户提供您一直享受的编辑器级类型安全性。
为此,您可以*.d.ts
在要发布的包的根目录中发布类型声明文件 ( )。只要在项目文件夹中找到这些声明文件,TypeScript 和 TypeScript 语言服务器就会默认遵循它们node_modules
。
对于 TypeScript 文件,这很简单,我们只需将这些选项添加到tsconfig.json
...
"noEmit": false,
"declaration": true,
...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;
}
(请注意,该文件的输出js
与我们在 js 版本中编写的完全相同。)
发布 JavaScript 库
遗憾的是,目前tsc
还不支持*.d.ts
从带 JSDoc 注释的文件生成文件。
我们希望将来能够实现,事实上,该功能的原始问题仍然有效,而且似乎已经支持3.7
。不要轻信我们的话,拉取请求正在提交中。
事实上,这种方法效果很好,我们在open-wc的生产中使用它。
警告!
这是一个不受支持的版本 => 如果某些功能无法正常工作,则无人会修复。
因此,如果您的用例不受支持,则需要等待 TypeScript 正式发布来支持它。
我们擅自发布了一个分叉版本typescript-temporary-fork-for-jsdoc,它只是上述拉取请求的副本。
为 JSDoc 注释的 JavaScript 生成 TypeScript 定义文件
现在我们已经掌握了所有信息。让我们开始行动吧💪!
- 用 JS 编写代码并在需要的地方应用 JSDoc
- 使用分叉的 TypeScript
npm i -D typescript-temporary-fork-for-jsdoc
-
tsconfig.json
至少具备以下条件:"allowJs": true, "checkJs": true,
-
通过 进行“类型检查”
tsc
,最好通过huskypre-commit
进行钩子操作 -
tsconfig.build.json
至少有"noEmit": false, "declaration": true, "allowJs": true, "checkJs": true, "emitDeclarationOnly": true,
-
生成类型
tsc -p tsconfig.build.types.json
,最好在CI中 -
发布您的
.js
和.d.ts
文件
我们在open-wc上就有这样的设置,到目前为止它运行良好。
恭喜你现在无需构建步骤即可获得类型安全🎉
您也可以随意查看此帖子的存储库并执行npm run build:types
或npm run lint:types
现场观看魔术。
结论
总而言之 - 为什么我们喜欢 TypeScript,即使它需要构建步骤?
归根结底有两点:
- 类型对于您和/或您的用户来说非常有用(类型安全、自动完成、文档等)
- TypeScript 非常灵活,并且支持“仅” JavaScript 的类型
更多资源
如果您想了解有关使用 JSDoc 实现类型安全的更多信息,我们推荐以下博客文章:
致谢
在Twitter上关注我们,或者关注我的个人Twitter。请务必访问open-wc.org
查看我们的其他工具和推荐。
感谢Benny、Lars和Pascal 的反馈,帮助我将我的涂鸦变成一个可理解的故事。
文章来源:https://dev.to/open-wc/generating-typescript-definition-files-from-javascript-5bp2