「译」ECMAScript 提案:类型注解(TypeScript 在未来可能被纳入 ECMAScript 中)

1,397 阅读35分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第19天,点击查看活动详情

原文标题:ECMAScript proposal: Type Annotations

原文链接:github.com/tc39/propos…

ECMAScript 关于类型注解的提案已进入 stage1,阿林看到了原文后觉得很惊讶,因为原文表达的意思是 tc39 是真的想把静态类型检查这样的功能写进 JS 这门语言里,而不是像现在这样的在开发阶段用 TypeScript,然后再编译成健壮的 JS。

要知道,涉及到语言底层的改动,要考虑的兼容性、历史包袱、性能等问题,可不是一个小工作,这正是阿林很惊讶的原因。

在阿林的认知里,一个项目的代码能正常运行,但是如果非要吃饱了撑去做优化或者做重构,那就说明这个项目的架构或者设计真的很烂,可维护性和可延展性很差,不足以支持这个项目后续的开发(或者说后续开发起来效率很低)。

那么大到语言的层面,也许 JS 真的遇到了一些瓶颈,需要逐渐从动态弱类型语言,逐渐转变到既支持动态弱类型,也支持静态强类型。

这样的转变一定是向着好的方向去发展,毕竟谁也不喜欢吃饱了撑干费力不讨好的事,我们这些底层开发者如此,那些大佬们同样如此,既然他们非常认真地去设计和讨论这个提案,是有一定原因的,这些原因原文有介绍。

原文详细地介绍了这个提案目前的状况、动机和一些具体内容,讨论了某些 TypeScript 语法或特性不在考虑纳入的范围以及一些有待商榷的内容,回答了一些开发者们关于这个提案的问题,原文大纲可以总结成如下图:

image.png

如果这个提案顺利推进,未来的 ECMAScript 可能会包含部分 TypeScript 语法。

阿林觉得本文很有参考意义,就机翻加修改,翻译了本文,分享给想要了解 JS 这门语言最新资讯的朋友们,下面是正文。

本文档自 2022 年 3 月的 TC39 全会讨论以来没有更新过。细节将在未来几天内发生变化。

ECMAScript 提案:类型注解

这个提案的目的之一是使开发者能够在他们的JavaScript代码中添加类型注解,允许这些注解被一个 JavaScript 外部的类型检查器检查。在运行时,JavaScript引擎会忽略掉这些注解。

这个提议的另一个目的是让开发者能够运行用 TypeScript、Flow 和其他静态类型的 JavaScript 超集编写的程序,而不需要转译。

该提案的暂定语法可在此查阅。

提案状况

目前处于阶段1

作者:

  • Gil Tayar
  • Daniel Rosenwasser (Microsoft)
  • Romulo Cintra (Igalia)
  • Rob Palmer (Bloomberg)
  • ...还有一群贡献者,详见 仓库贡献历史.

负责人:

  • Daniel Rosenwasser (Microsoft)
  • Romulo Cintra (Igalia)
  • Rob Palmer (Bloomberg)

提案动机

在过去的十年中,静态类型检查的案例已经被证明是相当成功的。微软、谷歌和 Facebook 分别发布了TypeScriptClosure CompilerFlow。它们都是对 JavaScript 进行增强,以获得生产力的提高,就像其他静态类型语言一样,包括在早期发现错误,以及强大的编辑器工具(代码提示)。

就 TypeScript、Flow 和其他语言而言,这些 JavaScript 的变体为在 JavaScript 中声明和使用类型带来了方便的语法。这种语法大多不影响运行时的语义,而且在实践中,将这些变体转换为普通 JavaScript 的大部分工作都是擦除类型。

社区使用和需求

2020 年和 2021 年的JS状态调查中,静态类型化是最需要的语言功能。

logo.png

此外,TypeScript 目前在 GitHub 的 State of the Octoverse 中被列为第四大最常用的语言,在 Stack Overflow 的年度开发者调查中,它自2017年以来一直被列入最受欢迎的前4种语言和最常用的10种语言。

JavaScript 构建和编译的趋势

类型语法在 JavaScript 中的兴起与下层编译(有时称为 "转译")的兴起相吻合。随着 ES2015 的标准化,JavaScript 开发者看到了精彩的新功能,但由于支持旧版浏览器的限制,他们无法立即使用。例如,一个箭头函数可以为开发者提供工效,但不能在每个终端用户的机器上运行。因此,Traceur、TypeScript 和 Babel 等项目通过将 ES2015 代码重写成可在旧版运行时运行的等效代码来填补这一空白。

因为类型语法在 JavaScript 中没有得到原生支持,所以必须有一些工具在运行任何代码之前删除这些类型。对于TypeScript 和 Flow 这样的类型系统来说,将类型清除步骤与语法降级步骤结合起来是有意义的,这样用户就不需要运行单独的工具。最近,一些捆绑器甚至开始做这两件事。

但随着时间的推移,我们预计开发人员对降级编译的需求将会减少。常青的浏览器已经成为常态,而在后端,Node.js和 Deno 使用了非常新的 V8 版本。随着时间的推移,对于许多 TypeScript 用户来说,编写代码和运行代码之间唯一必要的步骤就是抹去类型注释。

构建步骤为编写代码增加了另一层关注。例如,确保构建输出的新鲜度,优化构建的速度,以及管理给调试带来便利的 sourcemaps ,这些都是 JavaScript 最初避开的问题。这种简单性使得 JavaScript 更加平易近人。

这个提案将减少对构建步骤的需求,这可以使一些开发设置变得更加简单。用户可以简单地运行他们编写的代码。

在 JSDoc 中写类型注释的局限性

虽然构建工具并不难用,但对于许多开发者来说,它们是另一个入门障碍。这也是 TypeScript 团队花精力支持在JSDoc 注释中表达类型的部分原因。JSDoc 注释在 JavaScript 社区有一些记录类型的先例,这些类型被 所利用。

这种注释惯例经常出现在构建脚本、小型 Web 应用程序、服务器端应用程序和其他地方,在这些地方添加构建工具的成本/收益权衡太高。即使 TypeScript 不提供类型检查诊断,注释惯例仍然在编辑器功能中被利用,因为TypeScript 为底层的 JavaScript 编辑体验提供动力。

下面是一个基于 JSDoc 的类型语法的例子,来源链接

/**
 * @param {string}  p1 - A string param.
 * @param {string=} p2 - An optional param (Closure syntax)
 * @param {string} [p3] - Another optional param (JSDoc syntax).
 * @param {string} [p4="test"] - An optional param with a default value
 * @return {string} This is the result
 */
 function stringsStringStrings(p1, p2, p3, p4="test") {
  // TODO
}

对比一下,下面是本提案所启用的等效的 TypeScript 语法。

function stringsStringStrings(p1: string, p2?: string, p3?: string, p4 = "test"): string {
  // TODO
}

JSDoc 注释通常更加冗长。除此之外,JSDoc 注释只提供了 TypeScript 支持的功能集的一个子集,部分原因是很难在 JSDoc 注释中提供表达式语法。

尽管如此,基于 JSDoc 的语法仍然是有用的,而且在 JavaScript 中对某种形式的类型注释的需求是很重要的,足以让 TypeScript 团队对它投入精力。

由于这些原因,本提案的目标是允许 TypeScript 语法的一个非常大的子集以原样出现在 JavaScript 源文件中,解释为注释。

提案内容

以下内容为草根提案

类型注解

类型注解允许开发者明确说明一个变量或表达式是什么类型。注解遵循声明的名称或绑定模式。它们以 : 开头,后面是实际的类型。

let x: string;

x = "hello";

x = 100;

在上面的例子中,x 被注解为字符串类型。TypeScript 等工具可以利用该类型,并可能选择在语句 x = 100 上出错;然而,遵循这一建议的 JavaScript 引擎会无误地执行这里的每一行。这是因为注解并不改变程序的语义,它等同于注释。

注解也可以放在参数上,以指定它们接受的类型,也可以放在参数列表的结尾处,以指定函数的返回类型。

function equals(x: number, y: number): boolean {
    return x === y;
}

这里我们为每个参数类型指定了数字,并为等价物的返回类型指定了布尔值

类型声明

很多时候,开发者需要为类型创建新的名称,以便它们可以很容易地被引用而不重复,并且可以递归地声明它们。

声明一个类型--特别是一个对象类型--的一种方法是使用一个接口

interface Person {
    name: string;
    age: number;
}

在接口的 {} 之间声明的任何东西都会被完全忽略。

虽然接口可以在TypeScript中扩展其他类型,但这里的规则有待讨论。

类型别名是另一种声明。它可以为更广泛的类型集合声明一个名字。

type CoolBool = boolean;

作为类型声明的类

这个提案将允许类成员,像属性声明和私有字段声明一样,指定类型注释。

class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }

    getGreeting(): string {
        return `Hello, my name is ${this.name}`;
    }
}

对于大多数类型检查器来说,被注解的类成员将有助于通过构造一个给定的类而产生的类型。在上面的例子中,类型检查器可以假设一个名为 Person 的新类型,其属性名称为 string,方法 getGreeting 返回一个字符串;但是就像本建议中的其他语法一样,这些注释并不影响程序的运行行为。

类型的种类

上面的例子使用了字符串、数字和布尔这样的类型名称,但是 TypeScript 和其他的类型支持具有更多涉及的语法,而不仅仅是一个标识符。下表中给出了一些例子。

名称例子
带有类型参数的类型引用Set<string>
对象类型{ name: string, age: number }
数组类型number[]
函数类型(x: string) => string
构造函数类型new (b: Bread) => Duck
元组类型[number, number]
联合类型string | number
交叉类型Named & Dog
this类型this
索引访问类型T[K]

上面的表格并不全面。

本建议的目的是找到一套合理的句法规则,以适应这些结构(以及更多),而不禁止现有的类型系统在这个领域进行创新。

这方面的挑战是表示一个类型的结束--这涉及到明确说明哪些标记可以和不可以成为注释的一部分。简单的第一步是确保匹配的小括号和大括号((...)[...]{...}<...>)内的任何东西都可以被立即跳过。再往前走就是事情变得更难的地方。

这些规则尚未建立,但在本提案推进时将会进行更详细的探讨。

具体可以看下面的链接,记录了提案的一些理念和想法。

参数的可选择性

在 JavaScript中,参数在技术上是 "可选的"--当参数被省略时,函数的参数将在调用时被分配为未定义的值。这可能是一个错误的来源,它是一个有用的信号,即一个参数是否真的是可选的。

要指定一个参数是可选的,可以在该参数的名称后面加上一个?

function split(str: string, separator?: string) {
    // ...
}

导入和导出类型

随着项目越来越大,代码被分割成模块,有时开发人员需要引用另一个文件中声明的类型。

类型声明可以通过在它们前面加上 export 关键字来导出。

export type SpecialBool = boolean;

export interface Person {
    name: string;
    age: number;
}

类型也可以使用导出类型语句来导出。

export type { SomeLocalType };

export type { SomeType as AnotherType } from "some-module";

相应地,另一个模块可以使用导入类型语句来引用这些类型。

import type { Person } from "schema";

let person: Person;

// or...

import type * as schema from "schema";

let person: schema.Person;

这些类型指定的声明也是作为注解。它们不会触发任何解析或包含模块。同样地,它们的命名绑定也没有被验证,因此,如果一个命名绑定引用了一个从未被声明的实体,也不会出现运行时错误。相反,设计时工具可以自由地静态分析这些声明,并在这种条件下发出错误。

纯类型的命名绑定

为值和类型维护单独的导入语句可能很麻烦--特别是当许多模块同时导出类型和值时。

import { parseSourceFile } from "./parser";
import type { SourceFile } from "./parser";

// ...

let file: SourceFile;
try {
    file = parseSourceFile(filePath);
}
catch {
    // ...
}

为了支持同时包含类型和值绑定的导入或导出语句,用户可以通过在它们前面加上 type 关键字来表达哪些绑定是纯类型的。

import { parseSourceFile, type SourceFile } from "./parser";

与上面的一个主要区别是,即使所有的绑定都被声明为纯类型,也会保留这样的导入。

// 仍然在运行时引入 "./parser"的内容.
import { type SourceFile } from "./parser";

类型断言

类型系统没有关于表达式的运行时类型的完美信息。在某些情况下,它们需要被告知某个位置的更合适的类型。类型断言--有时被称为转换--是断言表达式静态类型的一种手段。

const point = JSON.parse(serializedPoint) as ({ x: number, y: number });

术语 "类型断言 "在 TypeScript 中被选择,以远离 "cast "的概念,因为它通常有运行时的影响。相比之下,类型断言没有运行时行为。

不可为空的断言

TypeScript 中最常见的类型断言之一是非空断言运算符。它使类型检查器相信一个值既不是空的也不是未定义的。例如,我们可以写 x!.foo 来指定 x 不能是空的,也不能是未定义的。

// 断言我们有一个有效的 "HTMLElement",而不是 "HTMLElement | null
document.getElementById("entry")!.innerText = "...";

在 TypeScript 中,非空断言操作符没有运行时语义,这项提议将类似地指定它;然而,有理由将非空断言添加为运行时操作符。如果运行时操作符更受欢迎,这可能会成为一个独立的提议。

泛型

在现代类型系统中,仅仅谈论拥有一个数组是不够的--通常,我们对数组中的内容感兴趣(例如,我们是否拥有一个字符串数组)。泛型给了我们一种方法来讨论像类型上的容器这样的东西,而我们讨论字符串数组的方法就是写Array<string>

像本建议中的其他内容一样,泛型没有运行时行为,会被JavaScript运行时忽略。

泛型声明

泛型类型参数可以出现在类型和接口的声明中。它们必须在标识符后以 < 开始,以 > 结束。

type Foo<T> = T[]

interface Bar<T> {
    x: T;
}

函数和类也可以有类型参数,但变量和参数不能。

function foo<T>(x: T) {
    return x;
}

class Box<T> {
    value: T;
    constructor(value: T) {
        this.value = value;
    }
}

泛型调用

开发者可以在 TypeScript 中明确指定泛型函数调用或泛型类实例化的类型参数。

add<number>(4, 5)
new Point<bigint>(4n, 5n)

上述语法已经是用户可能依赖的有效的JavaScript,所以我们不能按原样使用这种语法。我们期望有某种形式的新语法可以用来解决这种模糊性。目前还没有提出具体的解决方案,但一个例子是使用语法前缀,如 ::

add::<number>(4, 5)
new Point::<bigint>(4n, 5n)

这些类型参数(::<type>)将被JavaScript运行时忽略。在TypeScript中也采用这种非歧义的语法是合理的。

this 参数

一个函数可以有一个名为 this 的参数作为第一个参数,this 参数(及其类型)会被运行时忽略。它对函数的长度属性没有影响,也不影响参数等值。

function sum(this: SomeType, x: number, y: number) {
    // ...
}

预计在一个箭头函数中使用 this 参数会被语法禁止,或者引发错误。

// Error!
const oops = (this: SomeType) => {
    // ...
};

有意地排除某些语法或特性

我们认为以下语法或特性明确排除在本提案的范围之外。

排除:生成代码的 TypeScript 特定功能

本提案不支持 TypeScript 中的一些语法或特性,因为它们有运行时语义,会生成JavaScript代码,而不是简单地被剥离和忽略。这些结构体不在本提案的范围内,但可以由单独的TC39提案添加。

排除: JSX

JSX 是对 JavaScript 的一种类似 XML 的语法扩展,旨在通过预处理器将其转化为有效的 JavaScript。它在 React 生态系统中被广泛使用,但它也被用于不同的库和框架。因为 JSX 直接与 JavaScript 代码交错,所以 JavaScript 的编译器和类型检查器通常也支持检查和转换 JSX。一些用户可能希望 ECMAScript 也能直接支持 JSX 转换,以扩大无需构建步骤即可处理的用例。

我们认为 JSX 不属于本提案的范围,因为。

  • JSX 是一个独立于可选静态类型的正交特性。本提案不影响通过独立提案将 JSX 引入 ECMAScript 的可行性。
  • JSX 的语法在转换时可以扩展为有意义的 JavaScript 代码。本提案只关注语法的消除。

有待商榷的内容

有几条语法可以干净利落地与 "类型作为注释 "的模式结合起来,但可能会觉得有点过火。我们认为,无论是否有这些语法,这个建议都是可行的,在某些情况下,我们提出了各种备选方案。

环境声明

偶尔有必要告知类型检查员某些值的存在,甚至模块的存在。类型系统有所谓的环境声明,其中一个声明将省略其实现。虽然类型系统通常有自己的 "声明文件 "格式,专门用于像这样的声明(例如TypeScript中的.d.ts文件,Flow中的.flow.js,等等),但在实现文件(即.js文件)中声明这些值也很方便。

在今天的类型系统中, declare关键字可以在变量、函数和类的声明等绑定之前。

declare let x: string;

declare class Foo {
    bar(x: number): void;
}

declare function foo(): boolean;

目前,该提案没有为环境声明保留空间,但这是一个选项。

函数重载

在现有的类型系统中,函数/方法的声明可以省略其主体。这被用于函数重载,它传达了一个函数的返回类型随其输入而变化。

function foo(x: number): number
function foo(x: string): string;
function foo(x: string | number): string | number {
    if (typeof x === number) {
          return x + 1
    }
    else {
        return x + "!"
    }
}

类修饰符

下列这些关键字都是在 TypeScript 和其他类型系统中的类型上下文之外使用的。

  • 抽象类和方法(abstract
  • 字段和方法的私有、保护和公共位置(privateprotectedpublic
  • 只读字段(readonly
  • 覆盖字段和方法(override

举个例子,这个提议可以支持以下语法。

class Point {
    public readonly x: number
}

如果这些被允许作为本提案的一部分,那么语义学上将忽略它们,将上述类别与以下类别同等对待。

class Point {
    x;
}

与类型一样,这些是 "软保证",在运行时不强制执行,但由类型检查器检查。可能需要有稍微不同的语法来禁止在这些新的上下文关键字后面添加新行。

包括这组关键字并允许它们被 "不正确地 "使用可能会让人感觉很奇怪。此外,随着时间的推移,可能会有更多的关键词被添加进来(例如,最近的覆盖)。

作为实现相同功能的一种方式,现有的类型系统可以在类型注释语法和现有的注释语法之间找到一个折中。例如,TypeScript 已经支持 JSDoc 中的一些修改器。

class Point {
  /**
   * @public
   * @readonly
   */
  x: number
}

可能还有另一个方向,这个建议扩展了注释语法,只是为了支持像这样的修饰语,用一个特定的符号引导。

class Point {
  %public %readonly x: number

  @@public @@readonly x: number
}

上述想法试图在不过度追求语法的同时,实现与现有类型系统的最大兼容性之间取得平衡。我们对这里提出的四个解决方案中的任何一个,或者人们可能有的其他想法持开放态度。

常见问题

JavaScript 需要静态类型检查吗?

考虑到各组织和团队在构建类型检查器和采用它们方面付出了多少努力,答案是肯定的。也许不是每个开发者都会使用静态类型检查,这也没关系--这就是为什么这个提议让类型注释完全可有可无;然而,生态系统对使用类型的需求是不可否认的。

TypeScript已经很好地证明了这一点--它已经获得了非常广泛的使用,开发者也希望继续使用它。它是选择性的,但它在生态系统中占有重要地位,如今TypeScript的支持被视为使用库的巨大优势。

问题不是JS是否应该有类型,而是 “JS应该如何与类型一起工作?” 一个有效的答案是,目前的生态系统提供了足够的支持,其中类型被提前单独剥离出来,但这个提议可能会提供比这种方法更多的优势。

为什么不在 TC39 中为 JS 定义一个类型系统呢?

TC39 有一个编程语言设计的传统,倾向于本地的、健全的检查。相比之下,TypeScript 的模型--对 JS 开发者来说非常成功--是围绕着非本地的、尽力的检查。TypeScript 风格的系统在应用程序启动时的检查成本很高,而且每次运行 JavaScript 应用程序时都是多余的。

此外,定义一个直接在浏览器中运行的类型系统,意味着改进的类型分析将成为 JavaScript 应用程序用户而不是开发者的突破性变化。这将违反网络兼容性的目标(即 "不要破坏网络"),所以类型系统的创新将变得几乎不可能。允许其他类型系统单独分析代码为开发者提供了选择、创新和自由,开发者可以随时选择不接受检查。

相比之下,试图为JavaScript添加一个完整的类型系统将是一个巨大的多年努力,可能永远不会达成共识。这个建议认识到了这一事实,同时也认识到社区已经发展出了它所满意的类型系统。

这个提议与 TypeScript 有什么关系?

这个提议是一个平衡的行为:在允许其他类型系统的同时,尽可能地兼容 TypeScript,同时也不要过多地阻碍JavaScript 语法的发展。我们承认完全兼容不在范围之内,但我们将努力使兼容性最大化,差异最小化。

这个建议需要 ECMAScript 和 TypeScript 本身的努力,其中 ECMAScript 扩展了其当前的语法,但TypeScript 做出了某些让步,以便类型能够适应这个空间。正如在其他地方提到的,一些结构(如枚举命名空间)已经被搁置,可以选择在 TC39 中单独提出。其他的仍有待讨论(比如类修饰符环境声明)。而对于其他的,则需要对当前的代码进行歧义化处理(比如箭头函数)。

TypeScript 将继续与 JavaScript 的稍有限制的类型语法一起存在。换句话说,所有现有的 TypeScript 项目将继续编译,而且没有现有的 TypeScript 代码库需要改变。但是,任何现有的 TypeScript 代码,如果不在JavaScript 的类型语法范围内,将无法运行--它仍然需要被编译掉。

TypeScript应该在 TC39 中被标准化吗?

TypeScript 在语法和类型分析方面的发展非常迅速,使用户受益。将这种演变与 TC39 联系在一起,有可能使这种好处受到影响。例如,TypeScript 的升级经常需要用户修复类型问题,因为规则发生了变化,而这通常被认为是“值得的”,因为发现了真正的 bug;然而,这种版本升级路径对于 ECMAScript 标准来说是不可行的。向标准化迈进需要更多的保守主义。这里的目标是让 TypeScript 这样的系统在不同的环境中更广泛地部署,而不是阻碍TypeScript 的发展。

TypeScript 应该被认可为 JS 的官方类型系统吗?

目前还不清楚为 JavaScript 建立一个官方类型系统意味着什么。例如,JavaScript 没有官方的 linter 或官方的 formatter。相反,有一些工具会随着时间的推移而逐渐流行和发展起来。

此外,类型检查器,如 Flow、Closure 和 Hegel 可能希望提案不是 TypeScript,而是他们自己,使开发人员能够使用他们的类型检查器来检查他们的代码。让这个提议只针对 TypeScript,会阻碍其他类型检查器的努力。在这一领域的友好竞争将有利于JavaScript,因为它可以实现实验和新的想法。

为什么不在各种系统中非官方地建立TS检查和编译?

一些系统,如 ts-nodedeno ,已经尝试了这一点。除了启动时的性能问题,一个共同的问题是跨版本和模式的兼容性,包括类型检查语义、语法和转译输出。这个建议不会包含所有这些功能的需求,但它会提供一个兼容的语法和语义来统一满足许多需求。

为什么不坚持使用现有的JS注释语法?

虽然可以在现有的 JavaScript 注释中定义类型,就像 Closure Compiler 和 TypeScript 的 JSDoc 模式那样,但这种语法更加啰嗦,而且不符合人体工程学。

考虑到在 TypeScript 之前,JSDoc 类型注释已经存在于 Closure Compiler中,而 TypeScript 的 JSDoc 支持也已经存在多年。虽然 JSDoc 中的类型经过多年的发展,但大多数类型检查的 JavaScript 仍然是用TypeScript 的语法写在 TypeScript 文件中。这与 Flow 的情况类似,Flow 的类型检查器可以分析现有JavaScript 注释中类似 Flow 的语法,但大多数 Flow 用户仍然只是使用直接注释/声明语法。

所有的JS开发者用转码的方式也用得好好的?移除类型解构步骤真的会有帮助吗?

JavaScript 生态系统已经慢慢地回到了一个无转码的未来。IE11 的日落和实施最新 JavaScript 标准的常青浏览器的兴起,意味着开发者可以再次运行标准的 JavaScript 代码而无需转码。浏览器和 Node.js 中的原生 ES 模块的出现也意味着,至少在开发中,生态系统正在努力走向一个甚至不需要捆绑的未来。尤其是 Node.js 的开发者,历来都避免转码,如今他们在无转码带来的开发便利和 TypeScript 等语言带来的开发便利之间纠结。

实施这一提议意味着我们可以将类型系统加入到这个 “不再需要转码的东西” 的列表中,并使我们更接近一个转码是可选的而不是必须的世界。

类型能否像 TypeScript 的 emitDecoratorMetadata 那样通过运行时反射来获得?

这里的提议与 Python 的类型有很大的不同,因为这个提议中的类型完全被忽略,不作为表达式来评估,也不作为元数据在运行时访问。

这个提案明确地没有对运行时反射采取立场,把进一步的工作留给了未来的提案。主要原因是这个提议并没有阻止这个领域的进一步工作,而是使其成为可能。这也是 Python 在向语言添加类型时采取的路线。

依靠装饰器元数据的用户可以根据需要继续利用构建步骤。

这个提议是否使所有 TypeScript 程序都成为有效的 JavaScript ?

TypeScript 中的大多数结构体都是兼容的,但不是全部,大多数不通过的结构体可以通过简单的 codemod 修改来转换,可以使它们既兼容 TypeScript 又兼容本提议。

更多信息请参见上文 有待商榷的内容有意地排除某些语法或特性 部分。

这个提议是否使所有的 Flow 程序都有效的 JavaScript?

Flow 与 TypeScript非常相似,因此大多数类型构造都可以使用,但有一个类似的注意事项,即一些类型可能需要用圆括号来包装,以便与本提议兼容。

两个不符合这个建议的结构是 类型转换(例如(x:number))和 不透明的类型别名(例如 opaque type Meters = number)。Flow 可以考虑在语言中对其进行修改,使其符合本建议,例如,采用 as 操作符作为(x: number)的替代品,以及 type Meters = (new number)。这个建议也可以为opaque 类型别名保留空间。

那.d.ts文件和 "libdef "文件呢?

声明文件和库定义文件分别被 TypeScript 和 Flow 用作一种 “头” 文件,描述值、它们的类型以及它们存在的位置。本提案可以安全地忽略它们,因为它不需要解释其中的类型信息的语义。TypeScript 和 Flow 将像今天一样继续阅读和分析这些文件。

这一提议是否意味着 TypeScript 开发人员必须修改他们的代码库?

不,TypeScript 可以继续是 TypeScript,对代码库没有兼容性影响或改变。这项提议将使开发者可以选择将自己限制在 TypeScript 的特定子集,该子集将作为 JavaScript 运行,而无需转译。

开发人员可能仍然希望出于其他原因使用 TypeScript 语法。

  • 使用 JavaScript 中不支持的某些语法特征(例如枚举和参数属性)
  • 与现有的代码库兼容,这些代码库可能会遇到某些以不同方式处理的语法边缘情况
  • 对 JavaScript 的非标准扩展/重新解释(例如实验性的传统装饰器、装饰器元数据或字段的 [[Set]] 语义)。

如果开发者决定迁移现有的 TypeScript 代码库以使用本提案中指定的 JavaScript 语法,本提案的目标是将修改降到最低。我们相信许多开发者会被移除一个构建步骤所激励,但其他人可以决定坚持使用 TypeScript 编译并享受语言的全部力量。

工具应该如何与 JavaScript 类型语法一起工作?

考虑到一些 TypeScript 的功能 超出了范围,而且标准的 JavaScript 不会像 TypeScript 那样快速发展,也不支持它的各种配置,对于许多工具来说,以更完整的形式支持 TypeScript 将继续具有优势,超出了可能被标准化的 JavaScript。

一些工具目前需要一个 “插件” 或选项来使 TypeScript 支持工作。这个提议意味着这些工具可以默认支持类型语法,形成一个标准的、无版本的、始终在线的类型语法的共同基础。对 TypeScript、Flow 等的全面和规范性支持可以在此基础上保持一个选择模式。

与 ReasonML、PureScript 和其他编译成 JavaScript 的静态类型语言的兼容性如何?

虽然这些语言可以编译成JavaScript,并且有静态类型,但它们不是JavaScript的超集,因此与本提案无关。

直接部署类型化源代码的能力是否会导致应用程序的臃肿?

回到一个代码在生产中使用之前不需要严格编译的世界,意味着开发者最终可能会部署更多的代码,而不是必要的。因此,对于远程服务的应用程序来说,线上的有效载荷更大,在加载时需要解析的文本也更多。

然而这种情况已经存在。今天,许多开发者省略了构建步骤,并运送了大量的注释和其他不相干的信息,例如,未分化的代码。如果使用情况对性能敏感的话,对用于生产的代码进行提前的优化步骤仍然是一种最佳做法。

有明确不检查的类型注解这个功能吗?

这个建议引入了在运行时明确不检查的类型注释。这样做的目的是为了最大限度地减少注释的运行时间成本,并提供一个一致的心理模型,其中类型不影响程序行为。一个潜在的风险是,用户可能没有意识到需要运行一个外部工具来查找类型错误,因此,当他们的类型注释代码中出现与类型相关的错误时,他们会感到惊讶。

对于今天的外部类型检查器的用户来说,这种风险已经存在。今天,用户通过以下的组合来减轻这种风险。

  • 进行类型检查并主动浮现类型错误的 IDE
  • 将类型检查集成到项目开发工作流程中,例如 npm 脚本、CI 系统、TSC

在某些方面,如果类型是在运行时检查的,对用户来说会更加惊讶。在其他语言中,运行时的类型检查是很常见的。例如,在C++中,除了一些已知的情况,如程序员要求,如 dynamic_cast,在运行时几乎没有检查。

正如在 TypeScript 中看到的那样,开发人员很快就会了解到类型在运行时没有作用。因此,类似于第一个问题 “JavaScript需要静态类型系统吗?”,这个问题的答案部分是由外部类型检查器的广泛成功所带来的。

在未来如何增加运行时检查的类型?

由于会带来运行时的性能开销,JavaScript 似乎不太可能采用一个普遍的运行时检查类型系统。虽然有可能有改进的余地,但过去的努力,如 TS*(Swamy等人)已经表明,基于注释的运行时类型检查会增加不可忽视的速度。这也是本提案赞同纯静态类型的原因之一。

如果希望保持语言的开放性,以便以后除了这里提出的静态类型之外,还能增加运行时检查的类型,我们可以在语法中明确保留语法,以支持这两种类型。

这个提案能否让运行时根据类型提示来优化性能?

虽然有可能在理论上实现由静态声明的类型驱动的运行时优化,但提案作者并不清楚在 JavaScript中是否有成功的实验可以有意义地击败动态类型驱动的 JIT 优化。

建议的类型语法没有定义语义,所以对运行时来说是不透明的。这相当于问 “运行时能否使用 /**注释**/ 来优化性能?” 答案是:几乎肯定不能--至少不能以标准的方式。因此,这个建议本身并没有直接提供新的性能改进机会。

明确地说,本提案的目标不是为了提高 JavaScript 的性能。

现有技术

其他具有可选择的可擦除类型语法的语言

当 Python 决定给语言添加一个渐进的类型系统时,它分两步进行。首先,在 PEP-3107 - Function Annotations 中创建了一个类型注释的提案,该提案规定了 Python 中的参数类型和函数返回类型 几年后,在 Python PEP-484 - Type Hints 中扩大了类型可以出现的地方。

这里的提议与 Python 的类型有很大的不同,因为这个提议中的类型完全被忽略,不作为表达式进行评估,也不作为元数据在运行时进行访问。这种差异主要是由现有的社区先例所引起的,JS 类型系统不倾向于对其类型使用 JS 表达式语法,所以不可能将其作为表达式来评估。

Ruby 3现在也实现了RBS:位于代码旁边的类型定义,而不是代码的一部分。

在 JavaScript 上添加类型系统的语言

TypeScript、Flow 和 Hegel 是在标准 JavaScript 之上实现类型系统的语言。

能够通过注释向 JavaScript 添加类型系统

TypeScript 和 Flow 都允许开发者编写 JavaScript 代码,并加入 JavaScript 运行时忽略的类型注释。

对于 Flow 来说,这些是 Flow 注释类型,而对于 TypeScript 来说,这些是 JSDoc 注释

请参阅作者的博文,了解他们对TypeScript的JSDoc注释的积极体验。

Closure Compiler 的类型检查完全通过 JSDoc 注释工作,文档地址。Closure Compiler 团队已经收到了许多关于内联类型语法的请求,但在没有标准的情况下,他们对这样做犹豫不决。

TC39 中的相关提案和讨论

TC39 之前已经讨论过 guards,它形成了一个新的、更强大的类型系统。

以前,Sam Goto领导了围绕一个可选类型提案的讨论,该提案旨在统一各类型检查器的语法和语义。试图在各类型检查器中找到一致意见,同时在语法和语义中定义一个足够的子集,意味着这种方法存在困难。这个计划的一个演变是可插拔类型,其灵感来自于 Gilad Bracha关于可插拔类型系统的想法。这个建议与可插拔类型的建议极为相似,但更倾向于将类型视为注释的想法,而且是在类型检查被更广泛地采用和类型检查生态系统更加成熟的情况下提出的。

可选类型建议库包含了其他围绕 JavaScript 中的类型所进行的讨论的链接

结尾和译者的一些思考

太长了,机翻加修改,总算翻译完了,吸收了太多信息,阿林脑袋要爆炸了。

能看完本文的,应该还是有收获的,如果你能把文中的很多链接的信息也吸收完,阿林佩服你,反正那些链接我是没看完。

TypeScript 在未来可能被纳入 ECMAScript 中,这个提案的落地阿林觉得会很艰难,很有可能无法落地

因为阿林觉得有一个很重要的点,就是如果真的这么做,JS 性能会变差,毕竟加了那些语法,总会在底层多处理很多东西的。

即使推行了这个提案,也会像原文说的那样,纳入部分 TS 语法,很多语法和特性都不会考虑,到时候还是会引 TS。

我倒是希望 TS 能像严格模式一样,成为一个可选项,供开发者自己选择要不要用。

对于我们这些普通开发者而言,保持学习,积极拥抱变化,饭碗不会丢的。

本文相对来说比较晦涩,推荐一篇我的文章,轻松学习 TS:

「1.9W字总结」一份通俗易懂的 TS 教程,入门 + 实战!

阿林水平有限,文中如果有错误或表达不当的地方,非常欢迎在评论区指出,感谢~

如果我的文章对你有帮助,你的👍就是对我的最大支持^_^

我是阿林,输出洞见技术,再会!