本文不讨论类型检查相关。
某些语法在新版 JavaScript 中已成为标准(如 ES2020 加入的 ?? 算符),本文不讨论这种从高版本到低版本的语法兼容转换,也不讨论装饰器语法。
纯类型项
TypeScript 自己声称 “TypeScript 就是 JavaScript 加上类型语法”(TypeScript is JavaScript with syntax for types)。这句话大体是对的,大多数 TypeScript 相比于 JavaScript 所增加的语法可以被简单逐字符替换为空格,因此有了 ts-blank-space 和 amaro 这种项目,其将 TypeScript 代码中类型相关的部分去掉,剩下的代码可以直接作为 JavaScript 代码运行。
可直接去掉的项,包括但不限于:
- 类型注释:
let s: string = ".." private,readonly关键字- 范型参数:
Uint8Array<ArrayBuffer> - 类型转换:
<A>a,a as A - 类的
extends关键字 import语句中的type标记:import { type A } from "a";- …
但实际上,TypeScript 存在一些语法,不能通过直接去除的方式成为 JavaScript 代码,见下文。
名称空间(Namespace)
namespace 在 TypeScript 1.5 之前的版本写作 module,那时还没有 ES Module,网页中的脚本全在同一个全局作用域中执行,人们常用 IIFE 来建立独有的作用域来避免污染全局空间,名称空间就是 IIFE 写法的语法糖。
namesapce 的编译方式是替换为 IIFE:
1 | // TypeScript |
其中可以执行语句,声明局部变量,就像在普通函数体中一样。
TypeScript 支持在名称空间中声明变量时加上 export 描述符,这样会把这个变量变成名称空间的属性,称为导出变量,例如上面示例中的 y,在 A 的内部和外部都转换成了 A.y。
类型也可以导出,写法如 export type T = number,这时简单删除即可,判断被导出的符号是值变量还是类型比较简单,和普通变量一样,看它的声明即可,type、interface 这些一定是类型。
麻烦之处在于,同一个名称空间可以在同一个文件中多次声明,也可以在不同文件中声明(在脚本模式下所有文件都视作在全局作用域运行,同名的名称空间运行时都是同一个变量),只要有一个名称空间中声明了变量,其他同名名称空间中使用这个变量都会作为该名称空间的属性来用。例如在上一个例子的基础上再加一个文件:
1 | namespace A { |
对应的编译结果是:
1 | var A; |
自动将 y 转换成了 A.y。但如果局部作用域中有名叫 y 的变量,就不会转换了:
1 | // TypeScript |
因此编译器在转换名称空间中的内容时,需要能记住符号是不是导出变量。在同一个文件内看来还是能解决的。而对于跨文件的情况,现在的第三方工具一般一次只转换单个文件,只能寄希望于随着 ES Module 的普及大家不要再使用 namespace 和脚本模式了。
import var = ... 和 export = ...
语法 import x = A.y; 只用于给 namespace 的导出变量起别名,后面跟的必须是 <namespace>.<item> 形式,可以是嵌套的 namespace,<item> 可以是类型或者变量,如果是类型,擦除即可;如果是变量的话,转换方式是将 import 替换为 var:
1 | // TypeScript |
而 import x = require("...") 语法是为了直接对应 CommonJS 的 var x = require("...") 写法同时又便于静态分析,转换方式是将 import 替换为 const(目标版本老于 ES6 时则替换为 var):
1 | // TypeScript |
export = ... 语法也是为了对应 CommonJS 的 module.exports = ... 写法,转换方式是将 export 替换为 module.exports,并移到文件末尾:
1 | // TypeScript |
参数属性(Parameter Properties)
TypeScript 的参数属性(Parameter Properties)语法支持在类构造器的参数前加 public/private/protected/readonly 修饰符来为类增加一个与该参数同名的属性。由参数属性增加的属性和普通属性一样,在 super() 调用后初始化,受 useDefineForClassFields 选项影响。注意,参数属性的参数可以在 super() 调用前被修改,结果是相应的属性使用修改后的参数值来初始化:
1 | // TypeScript |
枚举(Enum)
enum 也是转换为 IIFE。对于枚举 E 的每一项枚举成员声明 x = expr 或者 x:
- 如果
expr是字符串常量,转换为E["x"] = expr - 如果
expr是数字常量或者其它,转换为E[E["x"] = expr] = "x" - 如果没有
expr且是第一个枚举成员,按expr是数字常量 0 来生成 - 如果没有
expr且上一个枚举成员是数字常量 k,按expr是数字常量 k + 1 来生成 - 其它没有
expr的情况转换为E[E["x"] = void 0] = "x"
对于“常量”的定义可参见 TypeScript 文档。除了文档中描述的,字符串的简单计算也被支持,如 "a" + "b";使用 const 定义的编译期常量也被支持,例如 const a = 1;,注意声明中不能有类型注释,并且初始化表达式也必须是没有 as string 等的常量表达式,否则编译器会认为是非常量,报错。
例子(有编译错误):
1 | // TypeScript |
这里的难点在于要编译期计算常量表达式和判断 const 变量是不是常量表达式。最大的问题是 const 变量可以跨文件和模块影响 enum 的生成结果。
const enum 的规则和 enum 一样,但是是纯类型的。可以以减少编译期优化为代价,总是按照 enum 规则生成,以支持单文件生成。