第一章
开始了解 TypeScript
在我们深入具体内容细节之前,本章将帮助你了解 TypeScript 的全貌。TypeScript 是啥?你应该怎么看待它?它跟 JavaScript 有啥关系?它的类型可空的还是不可空?any 是怎么一回事?ducks 呢?
说 TypeScript 是一种语言有一点牵强,因为它既不运行在解析程序(Python 跟 Ruby),也不编译成更底层的语言(Java 跟 C)。相反,它编译成另一种更高级的语言 JavaScript。最终运行的是 JavaScript 而非 TypeScript。 所以,TypeScript 的本质是 JavaScript,这也是困惑的来源。弄明白这里面的联系将帮助你成为一个更加高效的 TypeScript 开发者。
TypeScript 的类型系统还有一些不同寻常的方面需要你注意的。后面的章节会介绍更多的细节,而这一章节会让你知道即将到来的一些惊喜。
第一节:弄懂 TypeScript 跟 JavaScript 的关系
如果你使用 TypeScript 很长时间了,那么你必然会听到这样话,“TypeScript 是 JavaScript 的超集” 或者“TypeScript 是 JavaScript 的类型超集”。但是,准确说来,这是什么意思呢?两者的关系是怎么样的呢?因为两者的联系太紧密了,充分理解它们之间的联系是用好 TypeScript 的基石。
TypeScript 是 JavaScript 在语法上的超集:如果你的 JavaScript 程序没有任何语法错误,那么它也是 TypeScript 程序。很可能 TypeScript 的类型检查器会标记代码中的一些问题。但这是一个单独的问题。TypeScript 会解析你的代码并生成 JavaScript。(这是它们之间关系的另外一个关键部分。我们将在第三节进行探索。)
TypeScript 文件用.TypeScript(或者 .TypeScriptx)后缀,好比 JavaScript 文件用.JavaScript(或者 .JavaScriptx)后缀。这并不意味着 TypeScript 完全是一种不同的语言!因为 TypeScript 是 JavaScript 的超集,所以在.JavaScript 文件中的代码其实也是 TypeScript。重命名 main.JavaScript 为 main.TypeScript 并不会改变这一点。
如果你正在迁移一个 JavaScript 代码库到 TypeScript,这就很有用。因为,这意味着你不需要用另外的语言重写任何代码,就可以使用 TypeScript,还能因此享受所带来的好处。如果你选择用 Java 重写 JavaScript,就不会有这种好事。这种绅士的迁移方式是 TypeScript 的特点之一。关于这个话题,第八章节会展开讲。
所有的 JavaScript 程序也是 TypeScript 程序,但是反过来不对,“有些 TypeScript 程序不是 JavaScript 程序”。这是因为 TypeScript 只是为特定的类型增加了额外的语法。(由于历史原因,它还添加了其他的语法。具体见第 53 节)
例如,这是一个合法的 TypeScript 程序:
function greet(who: string) {
console.log('Hello', who);
}
但是,当你通过 node 运行的时候,因为 node 能运行是 JavaScript,所以就会报错:
function greet(who: string) {
^
SyntaxError: Unexpected token :
上面代码中的 string 是 TypeScript 中特定的类型注释。一旦使用了一个,就不是普通的 JavaScript 了(见图 1-1)。
图 1-1.所有 JavaScript 是 TypeScript,但不是所有的 TypeScript 是 JavaScript
这并不是说,TypeScript 对普通的 JavaScript 程序没价值。其实是有的,举个小例,看下面这个 JavaScript 程序:
let city = 'new york city';
console.log(city.toUppercase());
运行的时候将会抛出一个错误: “TypeError: city.toUppercase is not a function”
在这个程序中没有类型注释,但是 TypeScript 类型检测器还是发现了这个问题:
let city = 'new york city';
console.log(city.toUppercase());
// ~~~~~~~~~~~ Property 'toUppercase' does not exist on type
// 'string'. Did you mean 'toUpperCase'?
不一定要告诉 TypeScript 变量 city 的类型是 string,因为通过初始值它能推断出类型。类型推断是 TypeScript 一个重要的部分,第三章节将会探索怎么用好它。
TypeScript 类型系统的其中一个目的,就是不用运行代码就能检测代码并抛出只有在运行时才会产生的异常。当你听到 TypeScript 被描述成一个静态的类型系统的时候,其实就是这个意思。 类型检测器不总是会发现有异常的代码,但它会尝试着去发现。
即使你的代码没有抛出异常,也并不意味着就是你想要的结果。而 TypeScript 会尝试着去捕获一些这种问题。例如,下面的 JavaScript 程序:
const states = [
{name: 'Alabama', capital: 'Montgomery'},
{name: 'Alaska', capital: 'Juneau'},
{name: 'Arizona', capital: 'Phoenix'},
// ...
];
for (const state of states) {
console.log(state.capitol);
}
// 将会打印:
undefined
undefined
undefined
哎呀呀!出了什么事?程序是合法的 JavaScript,也没有报任何错误。但是它就不是你想要的结果。即使没有加类型注释,TypeScript 的类型检测器也能发现错误,并且提供有用的建议。如下:
for (const state of states) {
console.log(state.capitol);
// ~~~~~~~ Property 'capitol' does not exist on type
// '{ name: string; capital: string; }'.
// Did you mean 'capital'?
}
即使不提供类型注释,TypeScript 也可以捕获错误,但如果提供了,它可以做到更加的细致缜密。这是因为类型注释告诉 TypeScript 你想干什么,还能发现那些不符合你预期的代码行为。 例如,你如果把上一个例子中 capital/capitol 的拼写错误颠倒过来呢?
const states = [
{name: 'Alabama', capitol: 'Montgomery'},
{name: 'Alaska', capitol: 'Juneau'},
{name: 'Arizona', capitol: 'Phoenix'},
// ...
];
for (const state of states) {
console.log(state.capital);
// ~~~~~~~ Property 'capital' does not exist on type
// '{ name: string; capitol: string; }'.
// Did you mean 'capitol'?
}
之前,这个错误很有用,但是现在完全错了!问题是你用两种不同方式拼写了同样的属性,TypeScript却不知道哪一个是对的。TypeScript可以猜,但是不一定每次都能猜对。解决办法是说明你的意图,明确的声明states的类型。
interface State {
name: string;
capital: string;
}
const states: State[] = [
{name: 'Alabama', capitol: 'Montgomery'},
// ~~~~~~~~~~~~~~~~~~~~~
{name: 'Alaska', capitol: 'Juneau'},
// ~~~~~~~~~~~~~~~~~
{name: 'Arizona', capitol: 'Phoenix'},
// ~~~~~~~~~~~~~~~~~~ Object literal may only specify known
// properties, but 'capitol' does not exist in type
// 'State'. Did you mean to write 'capital'?
// ...
];
for (const state of states) {
console.log(state.capital);
}
现在,报错跟修复建议就是对的。通过阐明我们的意图,也能帮助TypeScript发现一些潜在的问题。例如,你在数组中,其中一个对象的属性拼写错误,以前不会报错,但如果有了类型注释,就会提示:
const states: State[] = [
{name: 'Alabama', capital: 'Montgomery'},
{name: 'Alaska', capitol: 'Juneau'},
// ~~~~~~~~~~~~~~~~~ Did you mean to write 'capital'?
{name: 'Arizona', capital: 'Phoenix'},
// ...
];
在下面的维恩图中,我们可以新加一个程序分组:通过类型检测的TypeScript程序(见图 1-2)
图 1-2. 所有的JavaScript程序都是TypeScript程序,但是只有部分JavaScript跟TypeScript通过了类型检测。
如果你感觉“TypeScript是JavaScript的超集”这句话是错的,可能是你想到了图中的第三种程序(通过了类型检测的TypeScript程序)。在实践中,这是跟我们日常使用TypeScript最相关的一点。一般情况下,当使用TypeScript的时候,我们会试着让代码通过所有的类型检测。
TypeScript的类型系统仿照JavaScript的运行时行为。如果你之前用的是严格运行时检测的语言,那么你可能会遇到一些意料之外的事。比如:
const x = 2 + '3'; // OK, type is string
const y = '2' + 3; // OK, type is string
上面的语句都通过了类型检测,即使它们是可疑的,在其它语言中会产生运行时错误的。它们的结果都是字符串“23”,确实是模拟了JavaScript的运行时行为。
不过,TypeScript确实在有些地方划定界限。下面的语句中都标识出了问题,即使它们在运行时不会抛出异常。
const a = null + 7; // Evaluates to 7 in JavaScript
// ~~~~ Operator '+' cannot be applied to types ...
const b = [] + 12; // Evaluates to '12' in JavaScript
// ~~~~~~~ Operator '+' cannot be applied to types ...
alert('Hello', 'TypeScript'); // alerTypeScript "Hello"
// ~~~~~~~~~~~~ Expected 0-1 argumenTypeScript, but got 2
TypeScript类型系统的指导性原则就是要模拟JavaScript的运行时行为。但是,在所有这些情况中,TypeScript认为这些奇怪的用法更像是错误导致的,而非开发者的意图,因此,它不是简单的模拟运行时行为。我们看之前的另一个 capital/ capitol 的例子,这个例子中的程序并未抛出异常(会打印undefined),但还是会标记出错误。
TypeScript是怎么决定什么时候去仿照JavaScript的运行时行为,什么时候不去仿照的呢?归根结底,这是一个偏好问题。既然采用了TypeScript,你就应该相信构建它的团队的判断。如果你喜欢上面例子中的写法,那么TypeScript也许并不适合你。
如果你的程序类型检测了,还可能会在运行抛出错误吗?答案是会。下面是个例子:
const names = ['Alice', 'Bob'];
console.log(names[2].toUpperCase());
当你运行的时候,会抛出:
TypeError: Cannot read property 'toUpperCase' of undefined
TypeScript假定这个数组的索引取值(访问权限)在范围之内,但不是这样的,而是会出现异常。
当你使用any类型的时候,未捕获的错误也经常会发生。这个情况我们在第5小节讨论,至于更多的细节就放在第五章了。
这些异常发生的根本原因,是因为TypeScript理解到的值类型和实际类型已经不一样了。可以保证静态类型准确性的类型系统才能被称为稳健。如果稳健对你来说很重要,你可以看一看其他语言,比如Reason 或者 Elm。这两种语言更好的保证运行时安全的同时,也付出了一个代价:不再是JavaScript的超集,因此,迁移工作将变得非常复杂。
应该记住的点
-
TypeScript是JavaScript的超集。话句话说就是,所有的JavaScript程序也是TypeScript程序。TypeScript有其自身的语法,因此,TypeScript程序一般来说不是合法的JavaScript程序。
-
TypeScript增加了仿照JavaScript运行时行为的类型系统,并试着去发现那些在运行时有异常的代码。但是不要指望它会标出每个异常。也有可能代码通过了检测,但在运行时还是会抛出异常。
-
TypeScript在很大程序上模拟了JavaScript的行为,也会有一些JavaScript允许的情景,但是TypeScript选择禁用的,比如用错误数量的参数来调用函数。这很大程度上取决于偏好。