第一章:走进TypeScript的类型世界
第一章:走进TypeScript的类型世界
想象一下,你走进了一间陌生的厨房,冰箱里摆满了各种食材。但是这间厨房的主人非常细心,他在每一个保鲜盒上都贴上了小标签,写明了里面装的是什么——有的写着“番茄”,有的写着“鸡蛋”,还有的写着“昨天剩下的宫保鸡丁”。有了这些标签,即使你从未打开过这个冰箱,也能快速知道每个盒子里大概装着什么,要找东西时再也不用手忙脚乱地翻找了。这种给物品贴标签标注类别的做法,其实在编程世界中也有一个相似的概念,我们称之为“类型”(Type)。
从JavaScript说起:为什么需要类型
JavaScript的诞生与辉煌
要理解TypeScript为什么要出现,我们得先聊聊JavaScript的故事。1995年,一个名叫Brendan Eich的程序员在仅仅10天之内,就创造出了JavaScript这门语言(最初叫做Mocha,后来改名为LiveScript,最后因为与当时正火的Java语言攀上关系,才改名为JavaScript——虽然这两门语言之间其实没有太大关系)。当时,网景公司需要在他们的浏览器中嵌入一种脚本语言,让网页能够具有一些简单的交互能力,比如验证表单、响应按钮点击等。
谁也没有想到,这个“赶工”出来的产物,后来会成为全球最流行的编程语言之一。如今,JavaScript已经不仅仅是网页端脚本语言的代名词,它还延伸到了服务端(Node.js)、移动应用(React Native)、桌面应用(Electron)、甚至机器学习(TensorFlow.js)等众多领域。如果你访问任何一个现代网站,几乎都可以找到JavaScript在背后默默工作的身影。
然而,就像任何快速诞生的语言一样,JavaScript在设计时也做了很多妥协和折中。其中最被人诟病的一个特性,就是它的“弱类型”特性。什么叫弱类型呢?简单来说,就是JavaScript在使用变量时,并不需要你提前声明这个变量是什么类型,你可以随时把一个数字变成字符串,也可以把字符串变成布尔值。这种灵活性在小型脚本中确实很方便,但当项目规模变大时,就会变成一场噩梦。
弱类型带来的困扰
让我们通过几个具体的例子来看看弱类型会造成什么问题。假设你正在开发一个计算用户年龄的程序:
```javascript function calculateAge(birthYear) { return 2024 - birthYear; }
const userAge = calculateAge("1995"); console.log("用户的年龄是:" + userAge); ```
这段代码看起来很正常,对吧?但当你运行它时,问题出现了。JavaScript在进行数学运算时,会尝试把字符串转换成数字。在很多情况下,这种自动转换能够正常工作,比如 `"2024" - "1995"` 会得到 `29`。但是,`2024 - "1995"` 的结果并不是你期望的 `29`,而是一个奇怪的数字:`29`——等等,这好像是对的?
让我们再试一个例子:
```javascript console.log("你的年龄是:" + 25); // 输出: "你的年龄是:25" console.log("你的年龄是:" + [25]); // 输出: "你的年龄是:25" console.log("你的年龄是:" + {age: 25}); // 输出: "你的年龄是:[object Object]" ```
看,这里就出现问题了。同样是做字符串拼接,数字、数组、对象会产生完全不同的结果。而且,JavaScript并不会报错,它只是默默地给你一个可能完全不是你想要的结果。在大型项目中,这种静默的错误是最危险的,因为它们可能在测试中逃过检查,却在生产环境中造成难以追踪的bug。
再看一个更极端的例子:
```javascript function sendEmail(user) { // 假设我们期望 user 有一个 email 属性 console.log("发送邮件到:" + user.email); }
sendEmail({ name: "小明", email: "xiaoming@example.com" }); // 正常 sendEmail({ name: "小红" }); // 正常(user.email 是 undefined) sendEmail(null); // 报错:Cannot read property 'email' of null sendEmail(undefined); // 正常(undefined.email 会返回 undefined) sendEmail("小明"); // 输出: "发送邮件到:undefined" ```
在JavaScript中,不同的“空”值(null、undefined、空字符串、0、false)都有微妙但重要的区别。一个函数可能因为接收到任何一种“空”值而表现异常,而JavaScript并不会提前告诉你这个函数应该接收什么类型的参数,你需要阅读文档或者源代码才能知道。这种不确定性在大规模协作开发中会造成严重的沟通成本和调试困难。
类型系统:给代码贴上标签
回到我们一开始提到的厨房例子。厨房主人给保鲜盒贴标签,本质上是在做什么?是在给每样东西做分类和标注。这种做法的好处是显而易见的:你要找鸡蛋,不用每个盒子都打开看看;新来帮忙的人也能快速上手;更重要的是,如果有人不小心把盐放进了标着“糖”的盒子里,你一眼就能发现问题。
类型系统做的事情与此类似。当你声明一个变量是数字类型时,TypeScript(或者任何强类型语言)会确保这个变量永远只存储数字。如果你试图把一个字符串赋值给它,编译器会直接报错,而不是等到程序运行时才发现问题。这种“早发现,早治疗”的策略,能够在开发阶段就消灭大量的潜在bug。
这就像如果你家的每个调味罐上都标明了是什么调料,而且罐子还设计成只能装对应类型的调料(糖罐的开口只能让糖勺伸进去,盐罐同理),那么即使在做饭时手忙脚乱,你也不太可能把盐当成糖放进去,因为物理设计就不允许这种情况发生。
TypeScript登场:JavaScript的超集
什么是TypeScript
TypeScript是由微软公司开发和维护的一门编程语言,于2012年首次发布。它的设计目标非常明确:在JavaScript的基础上,添加可选的静态类型系统。请注意“可选”这两个字——TypeScript并不是要完全替代JavaScript,它是在JavaScript的基础上加了一层类型系统。这意味着你可以在同一个项目中逐步引入TypeScript,从最简单的类型注解开始,慢慢享受类型系统带来的好处。
从技术角度来看,TypeScript是JavaScript的超集(Superset)。这意味着任何有效的JavaScript代码,同时也就是有效的TypeScript代码。你可以把现有的JavaScript项目直接改个扩展名(从.js改成.ts)就能得到一个“合法的”TypeScript项目。当然,如果想要真正发挥TypeScript的威力,你还需要配置编译选项,并逐步添加类型注解。
TypeScript代码不能直接在浏览器或Node.js环境中运行,它需要先被“编译”(或者说“转译”)成JavaScript代码。这个编译过程由TypeScript官方提供的编译器(叫做`tsc`)完成。编译完成后,你得到的JavaScript文件就可以在各种JavaScript运行环境中正常执行了。
``` ┌─────────────┐ tsc 编译器 ┌─────────────┐ │ TypeScript │ ─────────────────► │ JavaScript │ │ (.ts) │ │ (.js) │ └─────────────┘ └─────────────┘ ```
TypeScript编译器的智能程度远超一般的语言编译器。它不仅能把TypeScript语法转换成JavaScript语法,还能进行类型检查——如果你的代码中存在类型不匹配的问题,编译器会在编译时报错,而不会等到运行时才暴露问题。
TypeScript的核心理念
TypeScript的设计哲学可以概括为三句话:渐进式增强、可选类型、与JavaScript保持兼容。
“渐进式增强”意味着你可以从零开始,一点一点地使用TypeScript的特性。不需要你一开始就给整个项目添加完整的类型定义,你可以先从一个文件、一段代码开始,先体验类型注解带来的好处。随着经验的积累,你可以逐步引入更高级的类型特性,比如泛型、条件类型、映射类型等。
“可选类型”是TypeScript区别于其他强类型语言的关键特性。在TypeScript中,类型注解不是强制的。你可以选择给某些变量添加类型,也可以暂时不添加。这与C++、Java等语言形成了鲜明对比——在那些语言中,你必须在编译时就确定所有变量的类型。TypeScript的这种灵活性大大降低了入门门槛,也让迁移现有JavaScript项目变得更加容易。
“与JavaScript保持兼容”则保证了TypeScript的生态系统和工具链与JavaScript世界是无缝衔接的。所有能在JavaScript中使用的库,TypeScript都能用;所有能运行JavaScript的环境,TypeScript代码编译后都能运行。TypeScript团队也在积极参与JavaScript新特性的制定和推广,确保TypeScript能够紧跟JavaScript的标准发展。
环境搭建:从零开始你的TypeScript之旅
安装Node.js和npm
在开始编写TypeScript代码之前,我们需要先准备好开发环境。TypeScript编译器本身是一个Node.js工具,所以第一步就是安装Node.js。
Node.js是一个JavaScript运行环境,它让你能够在服务器端、桌面应用甚至命令行工具中执行JavaScript代码。TypeScript编译器需要Node.js才能运行,所以请先访问Node.js官方网站(https://nodejs.org/),下载并安装最新版本的Node.js。
安装完成后,打开你的命令行终端(Windows用户可以使用PowerShell或CMD,macOS和Linux用户可以使用Terminal),输入以下命令检查安装是否成功:
```bash node --version npm --version ```
如果看到类似 `v20.10.0` 和 `10.2.0` 这样的版本号输出,说明Node.js和npm已经成功安装了。npm是Node.js的包管理器,全称是"Node Package Manager",它就像一个巨大的应用商店,你可以从中下载和使用各种各样的JavaScript/TypeScript工具库。
安装TypeScript编译器
有了npm,安装TypeScript就变得非常简单。TypeScript的官方编译器包名字就叫`typescript`,你可以通过npm全局安装它:
```bash npm install -g typescript ```
全局安装意味着你可以在任何目录下使用`tsc`命令。安装完成后,运行以下命令验证TypeScript是否安装成功:
```bash tsc --version ```
你应该能看到TypeScript的版本号,比如 `Version 5.3.3`。
选择你的IDE:Visual Studio Code
除了编译器,一个好用的代码编辑器(IDE)也是开发过程中不可或缺的工具。VS Code(Visual Studio Code)是目前最受欢迎的TypeScript开发工具,它由微软开发,与TypeScript有着天然的契合度。
VS Code对TypeScript的支持是开箱即用的。当你打开一个.ts文件时,VS Code会自动识别TypeScript语法,并提供:
- **语法高亮**:不同类型的代码元素用不同颜色显示,让代码结构一目了然
- **智能提示**:输入代码时,编辑器会猜测你接下来要写什么,并给出建议
- **实时错误检查**:即使不运行程序,编辑器也能发现代码中的类型错误
- **代码重构**:方便地重命名变量、提取函数等
- **调试支持**:内置调试功能,可以在代码中设置断点,逐步执行
VS Code还有一个巨大的优势:它的扩展生态系统。有很多优秀的扩展可以让你的TypeScript开发体验更加顺畅:
- **ESLint**:代码风格检查工具,可以帮助你保持代码风格的一致性
- **Prettier**:代码格式化工具,让你的代码自动排版得整整齐齐
- **GitLens**:增强版的Git工具,让版本控制更加直观
你可以通过访问 https://code.visualstudio.com/ 下载VS Code,安装过程非常简单,几乎是“下一步、下一步、完成”的流程。
创建你的第一个TypeScript项目
现在,让我们开始创建第一个TypeScript项目吧!打开你的命令行终端,在一个合适的目录下(比如你的代码工作区),依次执行以下命令:
```bash
创建项目文件夹
mkdir my-first-typescript cd my-first-typescript
初始化npm项目
npm init -y
安装TypeScript作为开发依赖
npm install typescript --save-dev ```
`npm init -y` 命令会创建一个`package.json`文件,这是Node.js项目的配置文件,记录了项目名称、版本、依赖等信息。`--save-dev` 参数表示我们将TypeScript作为开发依赖安装,因为它只是开发时需要的工具,不需要打包到最终产品中。
接下来,我们创建一个`tsconfig.json`文件,这是TypeScript的配置文件,告诉编译器应该如何处理我们的代码:
```json { "compilerOptions": { "target": "ES2020", "module": "commonjs", "strict": true, "outDir": "./dist", "rootDir": "./src", "esModuleInterop": true }, "include": ["src/**/*"], "exclude": ["node_modules"] } ```
让我们解释一下这些配置的含义:
- `target`: 指定编译后的JavaScript代码使用的ECMAScript版本。ES2020是一个比较现代的版本,被大多数运行环境支持
- `module`: 指定模块系统的类型。commonjs是Node.js默认使用的模块系统
- `strict`: 启用严格模式,这会开启一系列更严格的类型检查规则,推荐始终保持开启
- `outDir`: 编译输出目录,我们会把编译后的.js文件放在这里
- `rootDir`: 源代码目录,我们的.ts文件放在这里
- `include`: 指定要包含在编译中的文件,这里是src目录下的所有文件
- `exclude`: 指定要排除的文件,node_modules不在其中
现在,在项目根目录下创建一个`src`文件夹,然后在里面新建一个文件`index.ts`:
```typescript // src/index.ts function greet(name: string): string { return "你好," + name + "!欢迎来到TypeScript的世界!"; }
const userName = "小明"; console.log(greet(userName)); ```
注意这里出现了一个新的语法:`name: string`。这叫做类型注解(Type Annotation),它明确告诉TypeScript编译器,`name`参数应该是一个字符串类型,函数`greet`的返回值也应该是字符串类型。这就是TypeScript给你的代码贴上的“标签”。
现在,在命令行中执行编译命令:
```bash npx tsc ```
如果一切顺利,你会在`dist`目录下看到生成的`index.js`文件。运行它试试:
```bash node dist/index.js ```
你应该能看到输出:`你好,小明!欢迎来到TypeScript的世界!`
恭喜你,你已经成功创建并运行了第一个TypeScript程序!
基本类型初探:TypeScript的类型世界
基础类型一览
TypeScript提供了丰富的基础类型,让我们从最常用的几种开始学习。
字符串类型(string)
字符串用于表示文本数据。在JavaScript中,字符串可以用单引号、双引号或反引号(模板字符串)来定义。在TypeScript中,我们用`string`来表示字符串类型:
```typescript let userName: string = "小明"; let greeting: string = '你好'; let template: string = `欢迎,${userName}!今天是${new Date().getFullYear()}年`; ```
数字类型(number)
JavaScript中的所有数字(无论是整数还是浮点数)都使用同一个类型表示,TypeScript沿用了这一设计。数字类型用`number`表示:
```typescript let age: number = 18; let height: number = 1.75; let negative: number = -10; let scientific: number = 2.5e6; // 科学计数法:2500000 let hex: number = 0xff; // 十六进制:255 let binary: number = 0b1010; // 二进制:10 let octal: number = 0o777; // 八进制:511 ```
你可能注意到了,TypeScript支持多种进制的数字字面量,这和JavaScript是兼容的。编译器能够识别这些不同的表示方式,并正确地推导出它们的类型都是`number`。
布尔类型(boolean)