快速开始 (Up and Running Quickly)
那么, 什么是 TypeScript 呢? 以及我们该怎么使用它? 事实上, 用 TypeScript 语言作者 Hejlsberg 的一句话来说, 它简单的就是 "TypeScript 生成 JavaScript". 我们所能看到使用 JavaScript 的地方, 它都可以使用 TypeScript 来生成所需的 JavaScript.
JavaScript 已经随处可见了. 你见到的越多, 越能发现 JavaScript 正运行在从前不曾想象得到的地方. 你所访问的每一个网站都在使用 JavaScript 来处理网站上的内容, 使得网站交互性更强, 可读性更好, 更加吸引人. JavaScript 的强大与灵活性意味着有越来越多的工具会迁移到线上. 那些我们曾经需要下载并安装程序来进行编写文档, 绘制流程图等, 我们现在都可以在简单的 Web 浏览器中完成.
JavaScript 的流行, 以及其简单性, 带来了全栈的运行环境. 现在 JS 可以运行在服务端, 使用 NodeJS (或简单的称为 Node); 使用 serverless 技术运行在云端; 甚至可以运行在嵌入式设备中, 通过针对微控制器片上系统进行构建的版本. Apache Cordova 项目是一个成熟的服务器, 它可以以原生应用的方式运行在移动端设备中. 也就是说, 通过 JavaScript, HTML, 和 CSS 可以创建移动端应用. 即使是桌面应用也可以使用 JavsaScript 来构建. 例如流行的编辑器 Visual Studio Code, 以及 Atom. 该桌面项目使用 Electron 框架. 同样, 它也是成熟的桌面项目, 并且可以运行与 Windows, macOS, 以及 Linux 上.
那么, 构建 JavaScript 应用程序意味着, 你所构建的内容可以运行在任意操作系统中, 使用任意架构框架, 或者使用任意的嵌入式设备. 在构建 JavaScript 应用中, TypeScript 发挥到了作用. JavaScript 是解释型语言, 它有很多的优点, 但是缺点也不少. 解释性语言没有编译步骤, 因此没有办法在代码运行之前检查代码中是否存在小的错误, 包括拼写或语法错误. TypeScript 是强类型的, 面向对象的语言. 它使用编译器来生成 JavaScript. 编译器会定位错误, 在代码中, 在解释运行之前.
本章对 TypeScript 语言进行介绍, 并介绍如何快速的运行与生成 JavaScript. 本章中, 我们会比较一些 JavaScript 代码与之对等的 TypeScript 代码. 来解释我们为什么使用 TypeScript 来编写 JavaScript, 以及可以获得什么好处. 你所写的 JavaScript 会更加健壮, 错误更少, 更易于阅读, 易于维护, 以及更易于重构.
本章中, 我们会包含下面主题:
- 配置一个简单的 TypeScript IDE
- 介绍 JavaScript 的不同版本
- 配置 TypeScript 项目
- 类型语法与基本类型
- 函数签名
- 使用第三方库
- 介绍声明文件
首先让我们从设置 TypeScript 开发环境开始.
一个简单的 TypeScript IDE
TypeScript 生成 JavaScript 是通过一个被称为编译的步骤来完成的. 也就是说, 你编写好 TypeScript 代码后, 你需要编译, 更准确的说是需要转换这个代码, 它便会生成 JavaScript 代码. 为了使用这个编译步骤, 你需要一个 Node 环境, 以及 TypeScript 编译器. 本节中, 我们会讨论 TypeScript 集成开发环境的配置.
Node 是一个 JavaScript 运行环境. 与浏览器可以解释并执行 JavaScript 一样, Node 也可以做同样的事情. 有一个命令行驱动环境, 也就是说你需要在终端中运行 JavaScript.
安装 Node 很简单, 只需要下载 Node 安装包, 从 Node 官方网站 (https://nodejs.org) 上下载与你所用操作系统相匹配的安装包, 然后运行它. 因为它是开源的, 因此你也可以下载源码, 如果你可以, 你甚至可以根据你的系统环境编译源码获得 Node. 要判断 Node 是否安装成功, 打开命令行终端, 输入:
node --version
若 Node 被正确的安装, 你应该可以看到类似于下面的输出:
v15.12.0
这里我们可以看到 Node 已经在控制台输出其版本号. 在编写本书时, 其版本号为 v15.12.0.
使用 npm
Node 包含一个命令行程序, 即 Node 包管理器 (npm), 其用于下载包并使得全局可用, TypeScript 就是其中之一. 这些包可以全部以全局的方式安装, 这样就可以在系统中直接使用了. 也可以局部的安装. 局部安装的包, 会在本地名为 node_modules 的子目录中安装. 而全局安装会在系统级的 node_moduloes 目录中安装. 要检查 npm 是否安装成功, 在命令行中输入:
npm --version
输出的结果与下面类似:
7.6.3
这里, 可以看到我的系统中版本号为 7.6.3.
要全局安装 TypeScript 编译器, 在命令行中输入下面命令:
npm install -g typescript
此时, 我们就以全局的方式安装了 typescript Node 包, 使用 -g 选项. 虽然包名为 typescript, 但是 Node 使用 tsc 来执行 TypeScript 编译器. 要验证 TypeScript 编译器是否安装成功, 在命令行中输入:
tsc --version
此时, 我们调用了 TypeScript 命令行编译器 (tsc), 并使用 --version 参数来显示当前编译器版本. 输出类似于:
Version 4.2.3
此时, 我们可以看到安装在系统中 TypeScript 的版本号. 本书中会使用的版本为 4.2.3.
Hello TypeScript
我们首先来验证一下, 从 TypeScript 来生成 JavaScript. 那么首先你需要一个文本编辑器, 并且没有什么编辑器能好过微软的 Visual Studio Code 的了, 我们将其简单称为 VSCode. VSCode 也是由 TypeScript 构建的, 也是为了编辑 TypeScript 而创建的. 它可以使用第三方插件, 因此可以作为很多编程语言的 IDE 来使用, 包括 Java, C, 以及 C++. 它是免费的, 并且使用 Electron 框架开发, 也就是说它可以运行在 WIndows, Linux, 以及 macOS 上. 它的安装也很简单, 只需要从 https://code.visualstudio.com/download 下载, 并运行即可.
假定你已经成功安装了 VSCode, 我们可以在命令行中创建一个新项目文件夹, 然后按照下面方式打开 VSCode:
mkdir ch01
cd ch01
touch hello_typescript.ts
code .
这里, 我们创建了一个新目录, 名为 ch01. 然后进入该目录. 然后创建一个新文件, 名为 hello_typescript.ts. 最后, 我们使用命令 code .
运行 VSCode. 这里的点, 表示当前目录. 这便会运行 VSCode, 效果如下:
初翻, 未润色, 细节全截图与源文档. 一看就是 ubuntu 用户.
VSCode 的布局与其他文本编辑器类似, 在左边, 带有一个资源管理器窗口 (Explorer Window), 右边的便是代码编辑区. 下面在文件 hello_typescript.ts 文件中编写一些 TypeScript 代码, 如下:
console.log(`Hello TypeScript`);
这一行代码, 等价于你在其他语言的参考书中引入的 "Hello World" 示例. 我们调用了名为 console 的全局对象的 log 函数. 并传入一个字符串 hello TypeScript
. 我们可以在命令行中执行 tsc 命令来从 TypeScript 文件中生成 JavaScript 文件, 操作如下:
tsc hello_typescript.tsc
此时, 我们调用了 TypeScript 编译器, 并使用一个命令行参数, 来表示我们需要编译哪一个 TypeScript 文件, 该文件会生成 JavaScript. 现在, 我们已经生成了名为 hello_typescript.js 的 JavaScript 文件. 我们可以使用下面的命令, 用 Node 来执行 JavaScript 文件:
node hello_typescript.js
这里, 我们启用了 Node 运行时环境, 并指定需要执行 hello_typescript.js 文件中的 JavaScript 代码, 命令行输出为:
hello TypeScript
至此, 已经完成了创建 TypeScript 文件. 并使用 TypeScript 编译器 (tsc) 来编译该文件, 然后生成 JavaScript. 最后使用 Node 来执行该 JavaScript.
现在, 我们已经有一个 TypeScript 集成开发环境了, 并且可以从 TypeScript 来生成 JavaScript. 下面我们类看看, 我们如何从同一个 TypeScript 源文件中生成不同版本的 JavaScript.
模板字符串与 JavaScript 版本
为了去介绍 TS 编译器能使得我们的 JS 开发更为便利, 让我们快速看看模板字符串, 和模板字面量. 在前面的代码片段中, 你可能注意到使用了反引号 (`) 来作为字符串 hello TypeScript
的界定符. 如果我们看看生成的 JavaScript 文件 (hello_typescript.js), 我们会看到 TypeScript 编译器会将该行代码修改为使用双引号的形式, 例如:
console.log("hello TypeScript");
使用反引号 (`) 来表示字符串, 可以直接在字符串中插入一些值, 例如:
var version = `es6`;
console.log(`hello ${ version } TypeScript`);
这里, 我们声明了一个局部变量 version
, 并赋值为 `ws6`
. 然后, 我们修改需要打印的字符串, 使用 ${ ... 变量名 ... }
语法将 version
插入到输出字符串中间的位置. 如果编译该文件, 然后看看生成的 JavaScript 文件, 我们会看到代码变成:
var version = "es6";
console.log("hello " + version + " TypeScript");
此时, 我们已经看到 TypeScript 编译了模板字符串, 生成了等价的 JavaScript 代码. 其中使用双引号 (") 来包裹每一个字符串, 并使用加号 (+) 来连接字符串.
原本使用模板字符串也是合法的 JavaScript, 但是使用它有一个问题. 模板字符串是后来 JavaScript 版本引入的语法特性, 尤其是 ES2015, 也就是我们常说的 ES6th, 或简称为 ES6. 这就是说, 你需要支持 ES6 的 JavaScript 运行时才可以使用模板字符串. 然而, TypeScript 可以根据你的 JavaScript 目标运行时来生成 JavaScript 代码.
如果你不了解 JavaScript 的版本, 也没有关系, 这也是一个热点的争议话题. 简单的说, 就是记住, 所有的网络浏览器最低支持 ES3, 其全称为 ECMA-262, 3rd 版本, 它发布于 1999 年, 已是 22 年前了.
随着 JavaScript 的广泛应用, 其语言特性已被标准化, 被称为 ECMAScript 标准. 从历史上看, 该标准需要很长的时间进行更新. 1999 年, 在 ES3 发布之后, 经过了超过 10 年的时间, 才发布了下一个版本, 即 ES5. 然后又经历了 6 年时间发布了 ES6. 但是, 在 2015 年第 6 版发布时, 新的标准将以年为单位进行发布.
但是, 请注意, 即使新的标准发布了, 但是也不表示所有的浏览器, 或准确的说, 是所有的 JavaScript 的运行时会立即支持新的版本.
现在, 大多数浏览器支持的是 ES5 的标准. 不幸的是, 我们还需要经过数年时间才可以说, 大多数浏览器已经支持 ES6 标准了. 这就是说我们需要留心我们的用户, 与目标运行时. 如果我们试图在运行与浏览器的代码中使用 ES6 的特性, 例如模板字符串. 但是早期的浏览器, 包括那些早期的手机端的浏览器, 都只能支持到 ES3 的标准.
jk: Fu..., 遇到过, 还不报错.
如前面所说, TypeScript 可以基于你需要的 JavaScript 版本来生成 JavaScript. 因此, 我们所写的 TypeScript 代码可以使用最新的 JavaScript 标准, 并且 TypeScript 可以正确的生成对应的 JavaScript. 晚一点, 我们就会讨论这个概念.
TypeScript 项目配置
TypeScript 使用名为 tsconfig.json 的配置文件, 其中存储了编译选项. 我们可以使用 tsc 命令行编译器来生成 tsconfig.json 文件, 只需要使用 --init 命令, 如下:
tsc --init
--init 选项会在当前目录中自动生成一个 tsconfig.json 文件, 该配置被使用, 包含其他内容, 以及目标 js 版本.
This --init option will automatically generate a tsconfig.json file within the current directory, which is used, amongst other things, to specify the target JavaScript version.
让我们快速看看这个文件, 如下:
{
"compilerOptions": {
"target": "ES3",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
这里, 我们看到 tsconfig.json 文件使用标准 JSON 语法来指定一个对象, 该对象中含有一个主属性, 名为 "compilerOptions", 并且在该主属性中含有几个子属性, 名为 target, module, strict 等. 其中 target 属性用于指定你所要生成的 JavaScript 的版本, 并且其值含有几个可选的值. 如果我们在 ES3 上双击, 然后按下 Delete 键, 在按下组合键 Ctrl + Space, VSCode 会显示该属性可以使用的值:
Visual Studio Code 显示可用的 JavaScript 版本
这里, 我们可以看到 VS Code 使用了智能提示, 或代码补全逻辑, 来建议可用的 JavaScript 版本. 让我们修改该值为 ES6, 然后看看生成的 JavaScript.
有了 tsconfig.json 文件, 我们不需要指定我们在编译时需要的 TypeScript 文件的名字; 我们可以在命令行中输入 tsc, 如下:
tsc
此时, TypeScript 编译器读取 tsconfig.json 文件, 并应用应用这些编译选项, 然后作用于当前目录与子目录下的所有 .ts 文件. 也就是说, 编译的 TypeScript 项目, 可能包含很多文件夹, 每一个文件夹中都有很多文件, 我们只需要在项目根目录中输入 tsc.
现在, 我们修改了我们需要的 JavaScript 的版本, 即 ES6. 下面我们来看看编译后的输出, 在文件 hello_typescript.js 文件中, 如下:
"use strict";
var version = `es6`;
console.log(`hello ${version} typescript`);
忽略文件顶部的 "use strict" 行, 我们可以看到生成的 JavaScript 与原始的 TypeScript 没有变化. 这表示编译器已经生成了支持 ES6 的 JavaScript 代码, 即使我们没有修改原始的 TypeScript 文件.
注意, 在后面的章节, 我们会讨论 tsconfig.json 文件的版本选项, 以及它如何影响我们的代码.
监视文件的改变
TypeScript 还有一个选项, 它会监视全部项目目录树, 如果文件改变, 它会自动的重新编译整个项目. 按照下面的方式, 我们可以按照这个模式运行 TypeScript 编译器:
tsc -w
这里, 我们在 tsc 命令中添加了 -w 命令行选项. 输出则会变成:
[3:13:43 PM] Starting compilation in watch mode...
[3:13:43 PM] Found 0 errors. Watching for file changes.
此时, 我们看到 TypeScript 编译器已经处于监视模式, 并且会监视项目文件夹中所有文件的改变.
现在我们有了一个简单基础的开发环境, 下面来探讨一些 TS 的主要特征, 然后看看怎么生成 js 更好.
TypeScript 基础
JavaScript 不是强类型的. 该语言是动态的, 因此它允许在运行中修改对象它的类型, 属性, 以及行为. 然而, TypeScript 是强类型的, 并使用这些类型强制约束我们使用的变量, 函数, 与对象.
在本节中, 我们会围绕 TypeScript 的语言基础来展开. 我们会从强类型的概念开始, 也就是我们所知的静态类型. 然后我们会介绍该语言常用的基本类型, 并看看在代码中如何识别变量的类型. 我们还会介绍函数签名, 并介绍如何在 VSCode 中调试代码, 最后看看如何导入第三方 JavaScript 库.
强类型
强类型, 也称为静态类型, 意味着在创建变量或函数中定义参数的时候就指定了类型. 一旦指定了类型, 我们就不能更改它了. 弱类型与之刚好相反, 而 JavaScript 是一种弱类型的语言.
作为弱类型的案例, 我们创建一个名为 test_javascript.js 的 js 文件, 并添加下面的代码:
var test = "a string";
console.log("test = ", test);
test = 1;
console.log("test = ", test);
test = function (a, b) {
return a + b;
}
console.log("test = ", test);
在代码片段的第一行, 我们声明了一个局部变量 test, 并将字符串 "a string" 赋值给该变量. 然后我们使用 console.log 函数将其值打印在控制台. 然后, 我使用数值 1 赋值给变量 test, 然后依旧将其值打印在控制台中. 最后, 我们将带有两个参数 a 和 b 的函数赋值给变量 test, 然后将其打印在控制台中. 如果要运行该代码, 输入 node test_javascript.js, 我们将得到下面结果:
test = a string
test = 1
test = function (a, b) {
return a + b;
}
此时, 我们可以清楚的看到我们对变量 test 做出的变化. 它从最初的字符串变为数字, 最后变成函数.
不幸的是, 在程序运行过程中修改变量的类型是很危险的. 如果你预期代码中变量是一个数字, 并试图用它进行算术运算, 但是它是一个字符串, 那么计算得到的结果就可能出现问题. 同样的, 如果变量是一个函数, 但是无意间将其修改为字符串, 那么整个模块功能就会突然丢失.
TypeScript 是强类型的语言, 也就是说如果你将变量声明为字符串, 那么在代码中, 你就只能将其作为字符串来使用. 因此, 我们该怎么为变量声明正确的类型? 实际上, TypeScript 引入了一个简单的符号, 使用冒号 (😃 来表示变量的类型, 例如:
var myString: string = `this is a string`;
此时, 我们已经声明了一个变量, 名为 myString, 后面带上一个冒号, 以及关键字 string, 最后对其赋值. 这个技术被称为类型注解, 它将变量 myString 的类型设置为字符串类型. 如果我们现在试图将数字赋值给它, 例如:
myString = 1;
TypeScript 会生成编译错误如下:
hello_typescript.ts:6:1 - error TS2322: Type '1' is not assignable to
type 'string'.
6 myString = 1;
~~~~~~~~
这里我们得到了 TS2322 错误, 它表示类型 '1' (其类型为数字) 无法赋值给已经标记为 string 类型的变量. 注意编译器的输出内容, 它会告诉我们哪一行生成的错误, 也就是说哪一行代码出现了错误.
基本类型
TypeScript 提供了一些基本类型可用于数据类型注解. 接下来我们来看案例, 我们将重点放在四个通用类型上, 分别为 string, number, bollean, 和 array. 我们会在后面的章节中涉及其余的类型, 包括 any, null, never 等. 但是现在, 我们仅仅关注这四个基本类型, 例如:
var myBoolean: boolean = true;
var myNumber: number = 1234;
var myStringArray: string[] = [ `first`, `second`, `third` ];
此时, 我们已经定义了名为 myBoolean 的变量, 它是 boolean 类型的, 并初识值为 true. 然后, 我们声明了变量 myNumber, 并设置为 number 类型, 初识化为 1234. 最后我们声明了由 string 数组构成的 myStringArray 变量, 它使用 [] 数组语法来定义. 那么, 下面的所有语句都是非法的:
myBoolean = myNumber;
myStringArray = myNumber;
myNumber = myStringArray[ 0 ];
此时, 我们尝试将 myNumber 的值赋值给 myBoolean 变量, 然后将 myNumber 的值赋值给 myStringArray 变量, 最后试图将 myStringArray 第一个元素的值赋值给 myNumber 变量. 那么会得到下面的错误信息:
error TS2322: Type 'number' is not assignable to type 'boolean'.
error TS2322: Type 'number' is not assignable to type 'string[]'.
error TS2322: Type 'string' is not assignable to type 'number'.
这个错误提示告诉我们, 无法将一个类型的数据赋值给另一个类型. 我们修正一下代码如下:
myBoolean = myNumber === 456;
myStringArray = [myNumber.toString(), `5678`];
myNumber = myStringArray.length;
console.log(`myBoolean = ${myBoolean}`);
console.log(`myStringArray = ${myStringArray}`);
console.log(`myNumber = ${myNumber}`);
这里, 我们将表达式 myNumber === 456 的值赋值给变量 myBoolean. 由于判等表达式的值是 Boolean 类型的, 所以其值为 true 或 false. 因此在本例中, 赋值运算符右边的类型与赋值运算符左边的类型是相同的, 因此是合法的 TypeScript.
同样的, 后面我们使用 .toString() 方法将 myNumber 转换为字符串, 然后将它与值 "5678" 用一对方括号 [ 和 ] 括起来, 构成一个数组. 最后, 我们将 myStringArray 数组的 length 属性赋值给变量 myNumber. 同样, 这是被允许的, 因为数组的 length 属性是 number. 这段代码的输出如下:
myBoolean = false
myStringArray = 1234,5678
myNumber = 2
此时, 我们可以看到 console.log 语句已经打印出我们想看到的结果. 变量 myNumber 不等于 456, 因此变量 myBoolean 的值为 false. myStringArray 的值现在为 1234 和 5678. 而 myNumber 的值是 myStringArray 数组的长度, 值为 2.
类型推断 (Inferred typing)
TypeScript 使用了一个叫做推断类型 (inferred typeing) 的技术, 或称之为类型推断 (type inference), 类确定变量的类型. 也就是说, 即使我们没有显式的指定变量的类型, 编译器也可以根据我们在声明它时, 为其提供的初始值来确定变量的类型. 同样, 一旦变量有了类型, 那么就会启用普通类型比较. 要说明这个含义, 考虑下面代码:
var inferredString = "this is a string";
var inferredNumber = 1;
inferredString = inferredNumber;
这里, 我们创建了两个变量, 分别为 inferredString 和 inferredNumber, 并为第一个变量赋值一个字符串, 为第二个变量赋值一个数字. 然后我们试图将变量 inferredNumber 赋值给 inferredString. 那么这段代码会显示下面的错误:
error TS2322: Type 'string' is not assignable to type 'number'.
这段错误消息告诉我们, TypeScript 已经推断出 inferredString 的类型是字符串, 即使我们没有使用 :string 类型语法, 显式的声明变量的类型. 同样的, TypeScript 也已经推断出变量 inferredNumber 的类型是数字了, 因此会出现错误.
注意:
VSCode 会显示变量的类型, 如果我们将鼠标置于变量的上方, 这个特征在判断变量的类型时很有用.
动态类型 (Duck typing)
TypeScript 为更为复杂的变量类型使用了动态类型 (鸭子类型, Duck Typing) 方法. 鸭子类型的含义是说: "如果它看起来像鸭子, 叫起来像鸭子, 那么它很有可能就是鸭子". 换句话说, 有两个变量, 如果它们有同样的属性与方法, 那么就认为它们有同样的类型. 为了验证这一特性, 考虑下面代码:
var nameIdObject = { name: "myName", id: 1, print() {} };
nameIdObject = { id: 2, name: "anotherName", print() {} };
这里, 我们定义了变量 nameIdObject, 它是一个标准的 JavaScript 对象, 含有 name 属性, id 属性, 以及 print 函数. 然后我们重新为其赋值为另一个对象, 这个对象也有 name 属性, 一个 id 属性, 和一个 print 函数. 编译器会使用动态类型来判断这个赋值是否合法. 换句话说, 如果一个对象含有相同的属性与方法的集合, 他们就被被认为是同一个类型.
jk: 翻译上将鸭子类型与动态类型视为同义词.
进一步展示这一特性, 看看编译器对我们试图打破这一规则会有什么反映, 例如:
nameIdObject = { id: 3, name: "thirdName" };
这里, 我们试图给 nameIdObject 赋值一个对象, 而该对象没有包含完全所需的属性. TypeScript 会生成下面的错误:
error TS2741: Property 'print' is missing in type '{ id: number; name:
string; }' but required in type '{ name: string; id: number; print():
void; }'.
此时, 错误消息高速我们 print 属性缺失, 因此 TypeScript 使用了动态类型检查来确保类型安全.
注意, 在给一个对象赋值时, 如果属性名, 属性类型, 以及属性数量与目标类型无法完全相同都会引发该错误.
下面看看另一个简单的鸭子类型的案例, 例如:
var obj1 = { id: 1, print() {} };
var obj2 = { id: 2, print() {}, select() {} };
obj1 = obj2;
obj2 = obj1;
此时, 我们有了两个对象, 分别是 obj1 和 obj2, 并且除了 obj2 中含有额外的 select 函数外, 其他完全相同. 然后我们将 obj2 赋值给 obj1. 这一步不会生成任何错误, 因为 obj2 的类型中含有 obj1 的类型中的所有属性. 这句话就是说, 鸭子类型检查 obj2 中若至少含有 obj1 中的所有属性, 那么它的类型就可以赋值的.
而最后一行代码, 我们将 obj1 的值赋值给 obj2, 则会生成下面的错误:
Property 'select' is missing in type '{ id: number; print(): void; }'
but required in type '{ id: number; print(): void; select(): void; }'
此时, 我们可以看到编译器正确的标识出了 obj1 的类型无法包含 obj2 的所有属性, 因为它缺少 select 函数.
jk: 看的不是现在它里面存储的值是否含有, 而是看最初初始化的时候是否含有该类型.
类型决定成员, 成员只能多不能少, 但是多了不会改变类型.
请记住, 鸭子类型的案例中也使用了类型推断, 因此对象类型是由其第一次赋值时推断而来的.
函数签名与 void
到此, 我们已经简单的介绍了 TypeScript 的基础. 它如何引入变量的类型注解, 它如何进行类型推断, 以及如何为赋值进行鸭子类型检查. 作为在 TypeScript 中使用类型注解最好的特性之一, 它可以用于强类型函数签名.
下面我们深入讨论一下这个特征, 我们编写一个 JavaScript 函数来执行一个计算, 代码如下:
function calculate(a, b, c) {
return (a * b) + c;
}
console.log("calculate() = " + calculate(2, 3, 1));
这里, 我们定义了一个 JavaScript 函数 calculate, 它有三个参数, 分别为 a, b, 和 c. 在函数中, 我们首先求 a 与 b 的乘积, 然后在加上 c 的值. 这段代码, 我们所预期的值为:
calculate() = 7
结果是正确的, 因为 2* 3 = 6, 而 6 + 1 = 7. 现在, 如果我们错误的使用字符串代替数字, 来调用函数, 来看看会发生什么. 代码如下:
console.log("calculate() = " + calculate("2", "3", "1"));
这段代码的输入为:
calculate() = 61
结果 61 与我们预期的结果 7 大相径庭. 这是为什么呢?
如果我们深入到 calculate 函数内部来观察, 我们就会发现在尝试混合不同类型的变量时 JavaScript 做了什么. 两个数的乘法, 即 ( a * b ) 会得到一个数字. 因此, 即使我们传入的 a 和 b 是一个字符串, JavaScript 都会试图将其转换为数字, 然后再求乘积. 因此, 我们还是会得到 3 * 2 = 6 的结果. 但是不幸的是, + 运算符可以用于数字和字符串, 并且如果一个操作数是字符串, JavaScript 会将两个操作数转换为字符串后在相加. 结果就是字符串 "6" 后面追加一个字符串 "1", 因此最后的结果是字符串 "61".
这个案例中的代码段说明了 JavaScript 会根据实际的使用方法来改变变量的类型. 这就表示为了 JavaScript 可以正确执行, 我需要注意类型转换的顺序, 并且理解其什么时候, 在哪里执行. 很显然, 这种自动类型转换的顺序会导致代码中不可预估的行为.
下面让我们使用 TypeScript 来实现其等价的代码, 但不同的是, 这里所有的参数类型必须是 number, 如下:
function calculate(a: number, b: number, c: number): number {
return (a * b) + c;
}
console.log(`calculate() = ${ calculate(3, 2, 1) }`);
这里, 在 Typescript 的 calculate 版本的函数中, 我们已经指定了参数 a, b, 和 c 必须是 number 类型. 除此之外, 我们还指定了函数的返回值为 number 类型, 在函数定义后添加了类型描述符, : number { ... }
.
这也是为了确保函数调用者会知道返回值的类型.
在这个指定了参数类型的函数上, 我们尝试传入字符串会报错, 如下:
console.log(`calculate() = ${ calculate("3", "2", "1") }`);
这时, TypeScript 会得到下面的错误:
error TS2345: Argument of type '"3"' is not assignable to parameter of
type 'number'.
这个错误告诉我们, 无法在指定为 number 的参数上使用 string 类型的参数. 我们也无法使用错误的返回值, 例如:
var returnedValue: string = calculate(3, 2, 1);
这段代码会得到下面错误:
error TS2322: Type 'number' is not assignable to type 'string'.
这里, TypeScript 编译器告诉我们 calculate 函数返回的类型是 number, 因此我们无法将其赋值给 string 类型的变量.
那么, 不返回值的函数是什么样的呢? 这便是 TypeScript void 关键字的用处. 考虑下面代码:
function printString(s: string): void {
console.log(a);
}
var returnedValue: string = printString("this is a string");
这里, 我们定义了函数 printString, 它仅有一个参数 a, 类型为 string. 我们还定义了函数不返回值, 使用 void 类型. 最后一行代码片段会导致下面错误:
error TS2322: Type 'void' is not assignable to type 'string'.
该错误告诉我们, 函数 printString 不返回任何数据, 即返回 void 类型. 若我们尝试将一个 void 类型赋值给一个 string, 我们就会得到该类型错误.