𝚲

当动态语言有了类型会是怎样的编程体验

类型一直存在于我脑子里,只是有些语言需要明说,另一些语言则假装和我很有默契。

图片来自 JavaScript vs TypeScript : Key Comparison

接上回,在用 JavaScript 快糙猛的蹚出一个原型后,我决定用 Scala.js 来重写,为得是让后续的迭代效率更高。但事情远没有那么简单,眼前出现是个精英怪,挡在主线情节上,绕不开,必须干。

尽管 Scala.js 让我拥有编写 Scala 的愉悦体验,但它本质是个将 Scala 代码编译成 JavaScript 代码的编译器后端扩展。也就是说, Scala 编写的部分,需要依赖的标准库和三方库,它们大多都是 JavaScript 编写的。同样,运行在 JVM 上 Scala 代码,依赖的是 Java 编写的标准库和三方库,必须考虑不同语言之间的互操作性(Interoperability)。

Scala.js 的作者是 Sébastien Doeraene 2,我发现国内某大号将其作者“颁发”给了 Li Haoyi 3,尽管他让 Scala.js 更为人所知,但他确实不是,我在此借机辅以澄清。

相比较从宽容的语言调用严格的语言,如 Javascript 调 Java 4,反过来从 Scala 调 JavaScript 就要麻烦一些。好在,这样精英怪不难打,就是费手。

往细了说,就是要为所依赖的 JavaScript 库 API 做类型适配性定义 5。类似的事情,TypeScript 也有做,这里借用官方文档的例子来说明 6

// JavaScript
const user = {
  name: "Hayes",
  id: 0,
};
// TypeScript
interface User {
  name: string;
  id: number;
}

const user: User = {
  name: "Hayes",
  id: 0,
};

如上适配工作,可能是细碎且一眼望不到头的,我也不想把精力放在这上面。开源社区里已经有一些前人的积累了 7,遗憾的是这些适配有可能版本落后,又或是不够完整,用起来会膈应。

上回关于语言的选择,有朋友在票圈认真评论道,“ AI coding 时代选 rust ”。按下语言选择不表,这类适配的辅助性工作,会不会才是 AI Copilot 该干的呢?

解决方案是有的,这要感谢 TypeScript 。不得不感慨,一个语言有多牛逼,还得看背后金主爸爸多有钱 8。我遇到的这个麻烦,早早地就困扰着 TypeScript 程序员 Boris ,为此 DefinitelyTyped 诞生了 9。 而后,相似故事又发生在 Scala 程序员 Oyvindberg 身上,才有了 ScalablyTyped 10。它能根据 DefinitelyTyped 中 TypeScript 类型定义,自动生成对应 Scala 类型定义。理论上,它就是个编程手柄,一键连招啊!

好是好,可也是有 隐形的认知成本 的,深究其中还蛮有趣的,我挑一个说说。TypeScript 中有个工具类型 Partial<Type> 11,典型的应用场景是,用来定义 配置选项 的对象类型,我试着用下面 Config 的代码来解释一下:

// TypeScript
interface Config {
  editable: boolean;
  showTips: boolean;
  ...
}

function input(conf: Partial<Config>): Input 

Partial<Config> 意思是,任何具有部分(也可以没有) Config 属性的对象都满足这个类型定义。通俗点讲,在调用input({...})时,Config 所有的属性都是可选的。像这样的情况在 JavaScript 里很常见,但是不是真的都可选,没有这样的类型定义,编译器也是没法帮我们预防隐患,只能靠运行时捉虫了。

当只看了某个 JavaScript 三方库的 API 文档时,我是不知道对应的 TypeScript 定义会是怎样,更是无法预料 ScalablyTyped 又是如何转译成 Scala 的。导致在 VSCode 里查找对应的 API 方法,我那个懵圈捉急啊。真是十指不沾阳春水,看着做好的饭菜我都不知道怎么下嘴。

ScalablyTyped 的转译结果,我稍作精简如下,

// Scala
trait Config:
  editable: Boolean
  showTips: Boolean
  ...

trait PartialConfig:
  editable: UndefOr[Boolean] = undefined 
  showTips: UndefOr[Boolean] = undefined 
  ???  

def input(conf: PartialConfig): Input 

type UndefOr[A]     = A | Unit
val undefined: Unit = ()

刚看上面,我是不能接受的,要我来手写会是,

// Scala
trait Config:
  editable: UndefOr[Boolean] = undefined 
  showTips: UndefOr[Boolean] = undefined 
  ???  

def input(conf: Config): Input 

Partial<Config> 的直译,显得画蛇添足,甚至违反直觉。转念细想,从 ScalablyTyped 角度看,其实是合理的。因为,它目的就是将 TypeScript 的翻译语义一致的 Scala, 而不关心 TypeScript 是不是从 JavaScript 那儿来的。那么,多此一举的会是 TypeScript 定义的锅吗?不好说,我更倾向把锅甩给 JavaScript ,谁叫它跟我玩默契。

以上窥见,是认知成本大,还是启用一门新语言成本大,得分情况。作为自己的项目我依然选择 Scala,但要是考虑到这个项目未来给其他人接手,嗯,不好说。

待续回见。

PS:说回本文的标题,要回答它吧,可能 TypeScript 程序员更有发言权,我这就是扔块砖吧。


  1. https://www.tatvasoft.com/blog/javascript-vs-typescript/↩︎

  2. https://lampwww.epfl.ch/~doeraene/↩︎

  3. https://www.lihaoyi.com/resume.html↩︎

  4. https://www.graalvm.org/latest/reference-manual/js/JavaInteroperability/#access-java-from-javascript↩︎

  5. https://www.scala-js.org/doc/interoperability/facade-types.html↩︎

  6. https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html#defining-types↩︎

  7. https://www.scala-js.org/libraries/facades.html↩︎

  8. 这个观点可能会引发嘴仗。这么说并不意味着可以无视语言生态里那些开源者的贡献,毕竟我自己勉强也算是其中一员。↩︎

  9. https://johnnyreilly.com/definitely-typed-the-movie#boris-yankov↩︎

  10. https://www.youtube.com/watch?v=R1Z_u2rEDj4↩︎

  11. https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype↩︎