tsc(TypeScript编译器)做了两件事:
1.它将下一代 TypeScript 转换为可以在浏览器中正常工作的旧版 JavaScript
2.它会检查你的代码是否有类型错误
令人惊讶的是,这两个行为完全是相互独立的。代码中的类型不能影响 TypeScript 生成 JavaScript。由于被执行的是 JavaScript,这意味着类型不会影响代码运行方式。
要记住的事:
- 代码的生成是独立于类型系统的。意味着 TypeScript 类型不能影响代码的运行时行为或性能。
- 一个有类型错误的程序也有可能产生代码的(“编译”)。
- TypeScript 类型在运行时是不可用的。要在运行时查询一个类型,你需要一些方法来重建它。标签联合和属性检查是常见的方法。一些构造,如class,既引入了 TypeScript 类型,有引入了一个在运行时可用的值。
类型错误的代码仍会产生输出
由于代码输出与类型检查无关,因此有类型错误的代码也可以产生输出:
// cat test.ts
let x = 'hello';
x = 1234;
// tsc test.ts // ~~ test.ts:2:1 TS2322: Type 'number' is not assignable to type 'string'.
// cat test.js 编译生成test.js
var x = 'hello';
x = 1234;
在存在错误的情况下仍输出代码是有实际帮助的。如果你正在构建一个 Web 应用程序,你可能知道它的某个部分有问题。但是因为 TypeScript 仍然会在存在错误的情况下生成代码,你可以在修复这些错误之前测试你的应用程序的其他部分。
当你提交代码时,应该以零错误为目标。如果你想禁用错误输出,你可以使用 tsconfig.json 中的 noEmitOnError 选项,或者在你的构建工具中使用等效的选项。
你无法在运行时检查 TypeScript 类型
看下代码:
interface Square {
width: number;
}
interface Rectangle extends Square {
height: number;
}
type Shape = Square | Rectangle
function calculateArea(shape: Shape) {
if (shape instanceif Rectangle) { // ~~ 'Rectangle' 仅代表的是类型,但在这里被用作一个值
return shape.width * shape.height; // ~~ 类型 “Shape” 上不存在属性 “height”
} else {
return shape.width * shape.width;
}
}
instanceof 检查是在运行时发生的,但 Rectangle 却是一个类型,所以它无法影响代码的运行时行为。TypeScript 类型都是 “可擦除” 的:编译成 JavaScript 的过程,其中一部分就是简单地从你的代码中删除所有的 interface、type 和类型注释。可以这么检查:
function calculateArea(shape: Shape) {
if ('height' in shape) { // 类型是 Rectangle
return shape.width * shape.height;
} else {
return shape.width * shape.width;
}
}
这是因为属性检查只涉及运行时可用的值,但仍然允许类型检查器将类型 shape 细化为 Rectangle 类型。
还有一种方法是引入一个标签,显式地将类型保留到运行时可用:
interface Square {
kind: 'square';
width: number;
}
interface Rectangle {
kind: 'rectangle';
height: number;
width: number;
}
type Shape = Square | Rectangle
function calculateArea(shape: Shape) {
if (shape.kind === 'rectangle') { // 类型是 Rectangle
return shape.width * shape.height;
} else {
return shape.width * shape.width;
}
}
这里的 Shape 类型就是一个 “标签联合类型” 的例子。因为它们可以非常容易地在运行时恢复类型信息,所以标签联合在 TypeScript 中无处不在。
还有一种修复错误的方法是可以将 Square 和 Rectangle 作为类。因为 class 会同时引入一个类型(在运行时不可用)和一个值(在运行时可用)。
class Square {
constructor(public width: number) {}
}
class Rectangle extends Square {
consctuctor(public width: number, public height: number) {
super(width);
}
}
type Shape = Square | Rectangle
function calculateArea(shape: Shape) {
if (shape instanceif Rectangle) { // 类型是 Rectangle
return shape.width * shape.height;
} else {
return shape.width * shape.width;
}
}
这个例子中,type Shape = Square | Rectangle 中的 Rectangle 指的是类型,而 shape instanceif Rectangle 中的 Rectangle 指的是值。尽管这种区别相当微妙,但对它的理解非常重要。
类型操作不能影响运行时的值
比如你有一个可能是字符串或数字的值,想把它正常化,使其总是一个数字。一下是一个错误的例子,尽管类型检查器接受了。
function asNumber(val: number | string): number {
return val as number;
}
// 看下生成的 JavaScript
function asNumber(val) {
reuturn val;
}
这里根本就没有进行任何转换。as number 是一个类型操作(类型断言),所以它无法影响你代码的运行时行为。为了使一个值正常化,你需要检查它的运行时类型:
function asNumber(val: number | string): number {
return typeof(val) === 'string' ? Number(val) : val;
}
TypeScript 类型对运行时的性能没有影响
因为当你生成 JavaScript 时,类型和类型操作会被清除,所以它们不可能对运行时性能产生影响。TypeScript 的静态类型是真正的零成本。
需要注意两个事项:
- 虽然没有运行时开销,但 TypeScript 编译器会引入构建时的开销。TypeScript 团队非常重视编译器的性能,编译速度通常相当快,特别是对于增量构建。如果开销变得显著,你也可以使用 “tanspile ony” 选项跳过类型检查。
- TypeScript 为支持旧的运行时而产生的代码可能会对原生实现产生性能开销。例如,如果你使用生成器函数,并以 ES5 为目标,而 ES5 比生成器出现的更早,那么 tsc 就会产生一些辅助代码来使其工作,这与生成器的原生实现相比,可能会有一些开销。