[Web翻译]从JavaScript生成TypeScript定义文件

2,775 阅读8分钟

原文地址:dev.to/open-wc/gen…

原文作者:dev.to/dakmor

发布时间:2019年9月4日 ・8分钟阅读

open-wc,我们是无构建开发设置的忠实粉丝。我们有篇关于它的文章😄。我们相信,未来都是要回归到web平台。这意味着依靠原生的浏览器功能,而不是依靠用户国度或JavaScript解决方案或开发工具。这就是为什么我们把为你这个开发者提供使用平台的工具技术作为我们的使命,甚至在传统浏览器最终被放弃之前。

这种方法赋予我们在DX、性能和可访问性方面的巨大优势,但也有缺点。众所周知,JavaScript是动态类型化的。开发者如果想在开发时享受类型检查,通常会使用微软的TypeScript、Facebook的Flow或Google的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')不会工作。

微软Visual Studio代码编辑器中helpers.test.ts的源代码截图,清楚地显示了第3行的错误信号,其中函数square是以字符串作为参数调用的。

如果我们在文件上运行TypeScript编译器tsc,我们会看到同样的错误。

$ 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文件中使用浏览器友好的导入语句,使用带有.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版本中,当调用string时,Visual Studio Code不再在第3行显示错误,用string

如果你在想 "哦,好吧,JavaScript是动态类型的,没办法了",那就看看这个:其实我们可以在vanilla JavaScript中实现类型安全,使用JSDoc注释。

使用JSDoc为JavaScript添加类型

JSDoc是JavaScript的一种历史悠久的内联文档格式。通常情况下,你可能会用它来为你的服务器的API或你的Web组件的属性自动生成文档。今天,我们将使用它来实现编辑器中的类型安全。

首先,给你的函数添加一个JSDoc注释。VSCodeatom的docblockr插件可以帮助你快速完成这个任务。

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

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

{
  "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完全相同的行为。

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.

重构

很好,我们已经写好了我们的square功能,包括类型检查,并推到了生产。但是过了一段时间,产品团队来找我们说,一个重要的客户希望能够在我们应用幂等化之前,为他们递增我们平方的数字。这个时候,产品团队已经和QA进行了沟通,QA连夜为我们重构的功能提供了以下测试。

expect(square(2, 10)).to.equal(14);
expect(square(2, 'ten')).to.等于(14)。

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

我们如何才能在保持类型安全的前提下,快速向客户提供这个关键的(😉)功能呢?

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

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

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

我们可以使用我们的vanilla-js-and-js-doc生产实现获得同样的效果。

/**
 * 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}偏移注解,但对于我们的目的来说,这是不必要的。

发布一个库

如果你希望人们能够使用你的代码,你就需要在某些时候发布它。对于JavaScript和TypeScript,这通常意味着npm。 你也希望为你的用户提供与你一直享受的编辑器级别的类型安全。 为了达到这个目的,你可以在你发布的包的根目录下发布类型声明文件(*.d.ts)。只要在一个项目的node_modules文件夹中找到这些声明文件,TypeScript和TypeScript语言Sever就会默认尊重它们。

对于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还不支持从JSDoc注释文件生成*.d.ts文件。 我们希望将来能支持,事实上,这个功能的原始问题仍然有效,而且似乎在3.7版中也会支持。不要相信我们的话,Pull Request正在飞行中。

事实上,这个功能非常好用,以至于我们在 open-wc 的生产中也在使用它。

警告!!!这是一个不支持的版本。 这是一个不支持的版本 => 如果有些东西不工作,没有人会去修复它。 因此,如果你的用例不被支持,你将需要等待TypeScript的正式发布来支持它。

我们擅自发布了一个forked版本typescript-temporary-fork-for-jsdoc,它只是上述pull request的一个副本。

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

那么既然我们已经掌握了所有的信息。让我们让它工作💪!

  1. 用JS写你的代码,并在需要的地方应用JSDoc。
  2. 使用forked TypeScript npm i -D typescript-temporary-fork-for-jsdoc
  3. tsconfig.json至少要有以下内容。
"allowJs": true,
"checkJs": true,
  1. 通过tsc进行 "类型化的打毛",最好是通过husky进行pre-commit的钩子。

  2. 有tsconfig.build.json,至少有以下内容

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

  2. 发布您的.js.d.ts文件。

我们在open-wc就有这样的设置,到目前为止,它为我们提供了很好的服务。

恭喜你,你现在已经拥有了不需要构建步骤的类型安全装置🎉

也可以随时查看这篇文章的仓库,并执行npm run build:typesnpm run lint:types来看看神奇的现场。

结论

总结一下--为什么我们是TypeScript的粉丝,尽管它需要一个构建步骤?

这归结为两点。

  • Typings对于你和/或你的用户来说是非常有用的(类型安全、自动完成、文档等)。
  • TypeScript是非常灵活的,它也支持 "仅仅 "JavaScript的类型。

更多资源

如果您想了解更多关于使用JSDoc进行类型安全的信息,我们推荐以下博客文章。

鸣谢

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

感谢BennyLarsPascal的反馈,并帮助我把我的涂鸦变成一个可追踪的故事。


通过www.DeepL.com/Translator (免费版)翻译