⚠️ 本文档未定期更新。请参阅2022年(3月29日、3月31日)和2023年(3月22日)的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)
请在issues中留下任何反馈!
动机:消除JavaScript的分叉
过去十年中,静态类型检查的案例已经被相当成功地证明。微软、谷歌和Facebook分别发布了TypeScript、Closure Compiler和Flow。这些都是对JavaScript的大规模投资,目的是获得他们在其他静态类型语言中看到的生产力提升,包括更早发现错误和利用强大的编辑器工具。
在TypeScript、Flow和其他类似工具的案例中,这些JavaScript变体带来了方便的语法来声明和使用JavaScript中的类型。这种语法大多不影响运行时语义,实际上,将这些变体转换为普通JavaScript的大部分工作仅仅是擦除类型。
对于人体工程学类型注解语法的强烈需求导致了带有自定义语法的JavaScript分支。这引入了开发者的摩擦,并意味着广泛使用的JavaScript分支在与TC39协调方面存在困难,必须冒语法冲突的风险。本提案将人体工程学语法空间正式化为注释,以整合类型检查的JavaScript分支的需求。
社区使用和需求
在State of JS调查中,"静态类型"是2020年、2021年、2022年和2023年最受欢迎的语言特性。
JavaScript编译趋势
类型语法在JavaScript中的兴起与向下级编译(有时称为"转译")的兴起同时发生。随着ES2015的标准化,JavaScript开发者看到了他们无法立即使用的wonderful新特性,因为需要支持旧浏览器。例如,箭头函数可以提供开发者的人体工程学,但无法在每个最终用户的机器上运行。因此,像Traceur、TypeScript和Babel这样的项目填补了这一空白,将ES2015代码重写为可在旧运行时工作的等效代码。
由于JavaScript本身不支持类型语法,因此在运行任何代码之前必须存在某种工具来删除这些类型。对于TypeScript和Flow等类型系统,将类型擦除步骤与语法降级步骤集成在一起是有意义的,这样用户就不需要运行单独的工具。最近,一些打包工具甚至开始同时执行这两项任务。
但随着时间的推移,我们预计开发者对降级编译的需求会减少。常青浏览器已成为常态,在后端,Node.js和Deno使用非常新的V8版本。随着时间的推移,对于许多静态类型系统用户来说,从编写代码到运行代码之间唯一必要的步骤将是擦除类型注解。
构建步骤为编写代码增加了另一层考虑。例如,确保构建输出的新鲜度、优化构建速度以及管理用于调试的源映射,都是JavaScript最初避免的问题。这种简单性使JavaScript更容易接近。
本提案将减少对构建步骤的需求,这可以使一些开发设置变得更加简单。用户可以直接运行他们编写的代码。
JSDoc类型注解的局限性
虽然构建工具的使用并不特别困难,但它们对许多开发者来说仍然是另一个进入障碍。这也是TypeScript团队投资支持在JSDoc注释中表达类型的部分原因。JSDoc注释在JavaScript社区中已经有一些先例用于记录类型,这些类型被Closure编译器利用。
这种注释约定通常出现在构建脚本、小型Web应用、服务器端应用以及其他添加构建工具的成本/收益权衡过高的地方。即使没有设置类型检查诊断,注释约定仍然被支持类型的编辑器在其底层JavaScript编辑体验中利用。
以下是来自TypeScript的JSDoc参考的基于JSDoc的类型语法示例。
/**
* @param {string} p1 - 一个字符串参数。
* @param {string=} p2 - 一个可选参数(Closure语法)
* @param {string} [p3] - 另一个可选参数(JSDoc语法)。
* @param {string} [p4="test"] - 带默认值的可选参数
* @return {string} 这是结果
*/
function stringsStringStrings(p1, p2, p3, p4="test") {
// TODO
}
以下是提议的语法,与大多数类型检查器兼容。
function stringsStringStrings(p1: string, p2?: string, p3?: string, p4 = "test"): string {
// TODO
}
JSDoc注释通常更冗长。此外,JSDoc注释只提供TypeScript支持的功能集的一个子集,部分原因是在JSDoc注释中提供富有表现力的语法很困难。
尽管如此,基于JSDoc的语法仍然有用,JavaScript中某种形式的类型注解的需求足够重要,以至于TypeScript团队对其进行投资,社区也创建了工具来支持使用JSDoc进行类型检查[1], [2]。
出于这些原因,本提案探索并期望更大的语法原样出现在JavaScript源文件中,被解释为注释。
提案
以下是第1阶段提案。请将其视为此类提案,预计会有更新,欢迎反馈。TC39阶段流程文档
类型注解
类型注解允许开发者明确声明变量或表达式的预期类型。注解跟在声明的名称或绑定模式之后。它们以:
开始,后面跟着实际类型。
let x: string;
x = "hello";
x = 100;
在上面的例子中,x
被注解为string
类型。进行静态类型分析的工具可以利用该类型,并可能选择在x = 100
语句上报错;然而,遵循本提案的JavaScript引擎将无错误地执行这里的每一行。这是因为注解不会改变程序的语义,它们等同于注释。
注解也可以放在参数上以指定它们接受的类型,以及在参数列表结束后指定函数的返回类型。
function equals(x: number, y: number): boolean {
return x === y;
}
这里我们为每个参数类型指定了number
,为equals
的返回类型指定了boolean
。
类型声明
大多数时候,开发者需要为类型创建新的名称,以便可以轻松引用而不重复,并且可以递归声明。
声明类型的一种方式 - 特别是对象类型 - 是使用接口。
interface Person {
name: string;
age: number;
}
interface
的{
和}
之间声明的任何内容都会被完全忽略。
类型别名是另一种声明。它可以为更广泛的类型集声明名称。
type CoolBool = boolean;
类作为类型声明
本提案将允许类成员,如属性声明和私有字段声明,指定类型注解。
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
getGreeting(): string {
return `Hello, my name is ${this.name}`;
}
}
对于大多数类型检查器来说,带注解的类成员将贡献于构造给定类时产生的类型。在上面的例子中,类型检查器可以假设一个名为Person
的新类型,带有string
类型的name
属性和返回string
的getGreeting
方法;但像本提案中的任何其他语法一样,这些注解不会影响程序的运行时行为。
类型种类
上述例子使用了像string
、number
和boolean
这样的类型名称,大多数静态类型检查器支持比单个标识符更复杂的类型语法。下表给出了一些例子。
名称 | 示例 |
---|---|
带类型参数的类型引用 | 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中,参数在技术上是"可选的" - 当省略参数时,函数的参数在调用时会被赋值为undefined
。
这可能是错误的来源,而且标明参数是否真的是可选的是一个有用的信号。
要指定参数是可选的,可以在该参数名后面加上?
。
function split(str: string, separator?: string) {
// ...
}
导入和导出类型
随着项目规模变大,代码被拆分成模块,有时开发者需要引用在另一个文件中声明的类型。
类型声明可以通过在前面加上export
关键字来导出。
export type SpecialBool = boolean;
export interface Person {
name: string;
age: number;
}
类型也可以使用export type
语句导出。
export type { SomeLocalType };
export type { SomeType as AnotherType } from "some-module";
相应地,另一个模块可以使用import type
语句来引用这些类型。
import type { Person } from "schema";
let person: Person;
// 或者...
import type * as schema from "schema";
let person: schema.Person;
这些指定了type
的声明也作为注释。
它们不会触发任何模块的解析或包含。
同样,它们的命名绑定也不会被验证,因此如果命名绑定引用了从未声明的实体,也不会产生运行时错误。
相反,设计时工具可以自由地对这些声明进行静态分析,并在这种情况下发出错误。
仅类型的命名绑定
维护单独的值和类型的导入语句可能很麻烦 - 尤其是当许多模块同时导出类型和值时。
import { parseSourceFile } from "./parser";
import type { SourceFile } from "./parser";
// ...
let file: SourceFile;
try {
file = parseSourceFile(filePath);
}
catch {
// ...
}
为了支持包含类型和值绑定的import
或export
语句,用户可以通过在绑定前加上type
关键字来表示哪些绑定仅为类型。
import { parseSourceFile, type SourceFile } from "./parser";
与上述方法的一个主要区别是,即使所有绑定都声明为仅类型,这样的导入也会被保留。
// 这仍然会在运行时导入"./parser"。
import { type SourceFile } from "./parser";
类型断言
类型系统并不能完全了解表达式的运行时类型。 在某些情况下,它们需要被告知在给定位置更合适的类型。 类型断言 - 有时称为类型转换 - 是断言表达式静态类型的一种方式。
// TypeScript
let value = 1 as number;
// Flow
let value = 1;
(value: number);
TypeScript中选择"类型断言"这个术语是为了与"类型转换"的概念保持距离,后者通常具有运行时含义。 相比之下,类型断言没有运行时行为。
非空断言
这是类型断言的一个常见用例,类型检查器从可空类型中过滤掉null,检查值是否既不是null
也不是undefined
。
例如,可以写x!.foo
来指定x
既不能是null
也不能是undefined
。
// 断言我们有一个有效的'HTMLElement',而不是'HTMLElement | null'
document.getElementById("entry")!.innerText = "...";
在TypeScript中,非空断言运算符没有运行时语义,本提案也会类似地规定它; 然而,将非空断言作为运行时运算符添加也有一定理由。 如果更倾向于运行时运算符,那可能会成为一个独立的提案。
泛型
在现代类型系统中,仅仅谈论有一个Array
是不够的 - 我们通常对Array
中的内容感兴趣(例如,我们是否有一个string
的Array
)。
泛型给了我们一种方式来谈论诸如类型的容器之类的东西,我们谈论string
的Array
的方式是写成Array<string>
。
像本提案中的其他内容一样,泛型没有运行时行为,会被JavaScript运行时忽略。
泛型声明
泛型类型参数可以出现在type
和interface
声明上。
它们必须在标识符后以<
开始,以>
结束:
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中或在Flow中。
add<number>(4, 5)
new Point<bigint>(4n, 5n)
上述语法已经是有效的JavaScript,用户可能依赖它,所以我们不能原样使用这种语法。
我们预期会有某种新的语法来解决这种歧义。
目前没有提出具体的解决方案,但一个可能的选择是使用语法前缀,如::
// 类型注解 - 示例语法解决方案
add::<number>(4, 5)
new Point::<bigint>(4n, 5n)
这些类型参数(::<type>
)会被JavaScript运行时忽略。
TypeScript采用这种无歧义的语法也是合理的。
this
参数
函数可以将名为this
的参数作为第一个参数,这个参数(及其类型)会被运行时忽略。
它对函数的length
属性没有影响,也不影响arguments
等值。
function sum(this: SomeType, x: number, y: number) {
// ...
}
预期在箭头函数中使用this
参数要么会被语法禁止,要么会触发早期错误。
// 错误!
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;
}
目前,此提案不为环境声明保留空间,但这是一个选项。
### 函数重载
在现有的类型系统中,函数/方法声明可以省略其主体。
这用于[函数重载](https://www.typescriptlang.org/docs/handbook/functions.html#overloads),表示函数的返回类型随其输入而变化。
```ts
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
类和方法private
、protected
和public
字段和方法的位置readonly
字段override
字段和方法
例如,此提案可以支持以下语法:
class Point {
public readonly x: number
}
如果这些被允许作为此提案的一部分,其语义将是忽略它们,将上述类视为与以下相同。
class Point {
x;
}
与类型一样,这些是"软保证",在运行时不强制执行,但由类型检查器检查。 可能需要稍微不同的语法来禁止在这些新的上下文关键字之后换行。
包含这组关键字并允许它们被"错误"使用可能会感觉奇怪。
此外,随着时间的推移可能会添加更多关键字(例如最近的 override
)。
作为实现相同功能的一种方式,现有的类型系统可以在类型注释语法和现有注释语法之间找到折衷方案。 例如,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 做出某些让步,以便类型可以适应该空间。
如前所述,一些构造(如 enum
和 namespace
)已被搁置,可选择在 TC39 中单独提出。
其他仍在讨论中(如 class
修饰符和环境声明)。
对于其他情况,将需要工作来消除当前代码的歧义(如箭头函数)。
TypeScript 将继续与 JavaScript 稍微受限的类型语法并存。 换句话说,所有现有的 TypeScript 项目将继续编译,并且不需要更改任何现有的 TypeScript 代码库。 但是,任何存在于 JavaScript 类型语法之外的现有 TypeScript 代码将无法运行 - 它仍然需要被编译掉。
TypeScript 应该在 TC39 中标准化吗?
TypeScript 在语法和类型分析方面都在继续快速发展,这对用户有利。 将这种演变与 TC39 绑定在一起可能会阻碍这种好处。 例如,TypeScript 升级通常需要用户修复类型问题,因为规则发生了变化,这通常被认为是"值得的",因为发现了真正的 bug; 然而,这种版本升级路径对 ECMAScript 标准来说是不可行的。 标准化的举措将需要更加保守。 这里的目标是使像 TypeScript 这样的系统能够在不同的环境中更广泛地部署,而不是阻碍 TypeScript 的发展。
TypeScript 应该被认定为 JS 的官方类型系统吗?
目前还不清楚为 JavaScript 设立一个"官方"类型系统意味着什么。 例如,JavaScript 没有"官方"的 linter 或"官方"的格式化工具。 相反,有一些工具随着时间的推移变得越来越受欢迎并不断发展。
此外,像 Flow、Closure 和 Hegel 这样的类型检查器可能希望使用这个提案来使开发者能够使用他们的类型检查器来检查他们的代码。 让这个提案只关注 TypeScript 可能会阻碍这种努力。 在这个领域的友好竞争对 JavaScript 有益,因为它可以促进实验和新想法。
为什么不非正式地在各种系统中内置 TS 检查和编译?
一些系统,如 ts-node 和 deno,已经尝试过这样做。 除了启动性能问题外,一个常见的问题是在类型检查语义、语法和转译输出方面跨版本和模式的兼容性。 这个提案不会取代所有这些功能的需求,但它会为许多需求提供一个兼容的语法和语义来统一。
为什么不坚持使用现有的 JS 注释语法?
尽管可以在现有的 JavaScript 注释中定义类型,就像 Closure 和 TypeScript 的 JSDoc 模式那样,但这种语法更加冗长和不人性化。
考虑到 JSDoc 类型注释在 TypeScript 之前就存在于 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 中的大多数构造都是兼容的,但并非全部, 而且那些不通过的大多数可以通过简单的代码修改转换, 使它们既与 TypeScript 兼容,又与这个提案兼容。
这个提案是否使所有 Flow 程序成为有效的 JavaScript?
Flow 与 TypeScript 非常相似,因此大多数类型构造都可以工作,
有类似的警告,即某些类型可能需要用括号包裹以与这个提案兼容。
两个不符合本提案的结构是类型转换(例如 (x: number)
)和不透明类型别名(例如 opaque type Meters = number
)。
Flow 可以考虑在语言中修改这些结构以符合本提案,例如采用 as
运算符作为 (x: number)
的替代方案,以及 type Meters = (new number)
。
本提案还可以为不透明类型别名预留空间。
.d.ts 文件和 "libdef" 文件怎么处理?
声明文件和库定义文件分别被 TypeScript 和 Flow 用作一种"头"文件,用于描述值、它们的类型以及它们所在的位置。 本提案可以安全地忽略它们,因为它不需要解释其中类型信息的语义。 TypeScript 和 Flow 将继续像现在一样读取和分析这些文件。
这个提案是否意味着 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 的超集,因此与本提案无关。
直接部署带类型的源代码是否会导致应用程序膨胀?
回到一个不需要严格在使用前编译代码的世界意味着开发者可能最终会部署比必要更多的代码。 因此,远程服务的应用程序会有更大的网络负载,加载时需要解析更多文本。
然而,这种情况已经存在。 现在,许多用户省略构建步骤,直接发布大量注释和其他多余信息,例如未压缩的代码。 对于性能敏感的用例,在生产环境中对代码进行预先优化仍然是最佳实践。
具体来说,请注意 TypeScript 编译器 tsc
没有内置的压缩选项。允许 TypeScript 用户在开发过程中跳过 tsc
不会本质上鼓励用户跳过压缩步骤,因为在任何情况下压缩 TypeScript 都需要一个单独的操作。
Babel 是另一个流行的 TypeScript、Flow 和 Hegel 转译器。Babel 的 preset-typescript
和 preset-flow
转译器不会进行压缩。在 Babel 中,压缩需要一个单独的步骤(通常由打包工具执行)。允许 Babel 用户省略 preset-typescript
不会鼓励用户跳过压缩。
在本提案中,类型注解被视为注释。使注释更有用、更易于使用是 JavaScript 的适当演进,即使我们期望开发者通过压缩来移除注释和类型注解。
未检查的类型是否是一个潜在的危险?
本提案引入了明确在运行时不检查的类型注解。 这是故意的,目的是最小化注解的运行时成本,并提供一个一致的心智模型,其中类型不影响程序行为。 一个潜在的风险是用户可能没有意识到需要运行外部工具来发现类型错误,因此当在他们带有类型注解的代码中出现与类型相关的错误时会感到惊讶。
对于当今使用外部类型检查器的用户来说,这种风险已经存在。 用户现在通过以下方式来缓解这种风险:
- 执行类型检查并主动显示类型错误的 IDE
- 将类型检查集成到项目开发工作流中,例如 npm 脚本、CI 系统、
tsc
在某些方面,如果类型在运行时被检查,对用户来说反而会更令人惊讶。
在其他语言中,运行时类型检查很少是常见的。
例如,在 C++ 中,除了一些已知的情况(例如当程序员请求时,如 dynamic_cast
)外,几乎没有运行时检查。
从 TypeScript 的经验可以看出,开发者很快就学会了类型在运行时不起作用。 因此,类似于第一个问题"JavaScript 是否需要静态类型系统?",这个问题在某种程度上已经被外部类型检查器的广泛成功所回答。
未来如何添加运行时检查的类型?
JavaScript 不太可能采用广泛的运行时检查类型系统,因为这会带来运行时性能开销。 虽然可能有改进的空间,但过去的努力,如 TS*(Swamy 等人),已经表明基于注解的运行时类型检查会带来不可忽视的性能下降。 这是本提案支持纯静态类型的原因之一。
如果希望保持语言对后续添加运行时检查类型的开放性,除了这里提出的静态类型外,我们可以在语法中明确保留以支持两者。
这个提案能否使运行时根据类型提示优化性能?
虽然可以理论上基于静态声明的类型进行运行时优化,但提案作者并不知道 JavaScript 中有成功的实验能够明显超越动态类型驱动的 JIT 优化。
提议的类型语法没有定义的语义,因此对运行时来说是不透明的。
这相当于问:"运行时能否使用 /** 注释 **/
来优化性能?"
答案几乎肯定是否定的——至少不会以标准的方式。
因此,这个提案本身并不直接提供新的性能改进机会。
明确指出,改善 JavaScript 的性能不是这个提案的目标。
先前的工作
其他具有可选可擦除类型语法的语言
当 Python 决定向语言添加渐进式类型系统时,它分两步完成。 首先,在 PEP-3107 - 函数注解中创建了类型注解提案,该提案在 Python 中指定了参数类型和函数返回类型。 几年后,PEP-484 - 类型提示扩展了类型可以出现的位置。
这里的提案与 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 中相关的提案和讨论
我们所知的最早的 JavaScript 类型提案是 Waldemar Horwat 在 2000 年 7 月的"Types"规范。
2008 年 ECMAScript 第 4 版的提案草案要求使用类型和可选的类型注解;TC39 在 2008 年同意撤回 ES4 提案,并在 2010 年发布 ES 3.1 作为 ES5。Auth0 有一篇博文简要介绍了 ECMAScript 4 的历史。
TC39 之前讨论过守卫,它们形成了一个新的、更强大的类型系统。 此前,Sam Goto 主导了关于可选类型提案的讨论,旨在统一各类型检查器的语法和语义。 试图在各类型检查器之间达成一致,同时在语法和语义上定义足够的子集意味着这种方法存在困难。 这个计划的演进是可插拔类型,它受到 Gilad Bracha 关于可插拔类型系统想法的启发。 这个提案与可插拔类型提案极为相似,但更倾向于将类型视为注释的理念,并且出现在类型检查得到更广泛采用和类型检查生态系统更加成熟的时期。
2015年,谷歌的V8团队尝试提出一项实现新JavaScript模式的提案,他们称之为"强模式",意图使用类型来提高网站性能。谷歌最终宣布实验失败。"最后我们不得不放弃这个想法。"
可选类型提案存储库包含了其他关于JavaScript类型的先前讨论链接:
- 2002 Javascript 2.0: 为不断发展的系统演进语言
- 2006 类型参数、类型系统和结构类型及初始化器的类型
- 2011 JavaScript的依赖类型 (pdf)
- 2011 守卫和商标
- 2014 TC39关于类型的讨论:添加语法而不添加语义。
- 2015 ES8渐进式类型及第二部分和ecmascript-types。