TS 概述
TS 是基于 JS 之上的编程语言,解决了 JS 自有类型系统的不足。通过使用 TS 可以帮我们提高代码的可靠程度。
语言类别
- 通过类型安全的维度区分编程语言,分为强类型和弱类型
- 强类型:
- 语言层面限制函数的实参类型必须与形参类型相同
- 有更强的类型约束
- 强类型语言中不允许任意的隐式类型转换
- 弱类型:
- 弱类型语言层面不会限制实参的类型
- 几乎没有什么约束
- 弱类型语言中允许任意数据的隐式类 型转换
- 强类型:
- 通过类型检查的维度区分编程语言,分为静态类型和动态类型
- 静态类型:
- 一个变量生命是它的类型就是明确的
- 声明过后,它的类型就不允许再修改
- 动态类型:
- 运行阶段才能够明确变量类型
- 变量的类型随时可以改变
- 动态类型语言中的变量没有类型,变量中存放的值是有类型的
- 静态类型:
JavaScript 是一门 弱类型 且 动态类型的语言,缺失了类型系统的可靠性
弱类型的问题
const obj = {}
obj.foo()
如上述代码,我们在书写代码阶段不会存在任何问题,直到代码运行,才会发现问题,若 foo 方法放置在隐晦的地方,无遗给我们的项目埋下了一颗定时炸弹💣
function sum(a, b) {
return a + b
}
sum(100, '100') // 会得到意料之外的结果
强类型的优势
- 错误更早暴露
- 代码更智能,编码更准确
- 重构更牢靠
- 减少不必要的类型判断
TS 本质
TypeScript 与 JavaScript 本质并无区别,可以将 TypeScipt 理解为是一个添加了类型注解的 JavaScript,比如 const num = 1
,它同时符合 TypeScript 和 JavaScript 的语法。
此外,TypeScript 是一门中间语言,最终它还需要转译为纯 JavaScript,再交给各种终端解释、执行。不过,TypeScript 并不会破坏 JavaScript 既有的知识体系,因为它并未创造迥异于 JavaScript 的新语法。
TS 特点
TypeScript 更加可靠
在业务应用中引入 TypeScript 后,当我们收到 Sentry(一款开源的前端错误监控系统)告警,关于“'undefined' is not a function”“Cannot read property 'xx' of null|undefined” 之类的低级错误统计信息基本没有。而这正得益于 TypeScript 的静态类型检测,让至少 10% 的 JavaScript 错误(主要是一些低级错误)能在开发阶段就被发现并解决。
我们也可以这么理解: 在所有操作符之前,TypeScript 都能检测到接收的类型(在代码运行时,操作符接收的是实际数据;静态检测时,操作符接收的则是类型)是否被当前操作符所支持。
当 TypeScript 类型检测能力覆盖到整个文件、整个项目代码后,任意破坏约定的改动都能被自动检测出来(即便跨越多个文件、很多次传递),并提出类型错误。因此,你可以放心地修改、重构业务逻辑,而不用过分担忧因为考虑不周而犯下低级错误。
接手复杂的大型应用时,TypeScript 能让应用易于维护、迭代,且稳定可靠。
面向接口编程
编写 TypeScript 类型注解,本质就是接口设计。
以下是使用 TypeScript 设计的一个展示用户信息 React 组件示例,从中我们一眼就能了解组件接收数据的结构和类型,并清楚地知道如何在组件内部编写安全稳定的 JSX 代码。
interface IUserInfo {
/** 用户 id */
id: number;
/** 用户名 */
name: string;
/** 头像 */
avatar?: string;
}
function UserInfo(props: IUserInfo) {
// ...
}
TypeScript 极大可能改变你的思维方式,从而逐渐养成一个好习惯。比如,编写具体的逻辑之前,我们需要设计好数据结构、编写类型注解,并按照这接口约定实现业务逻辑。这显然可以减少不必要的代码重构,从而大大提升编码效率。
同时,你会更明白接口约定的重要性,也会约束自己/他人设计接口、编写注解、遵守约定,乐此不疲。
TypeScript 已经成为主流
相比竞争对手 Facebook 的 Flow 而言,TypeScript 更具备类型编程的优势,而且还有 Microsoft、Google 这两家国际大厂做背书。
另外,越来越多的主流框架(例如 React、Vue 3、Angular、Deno、Nest.js 等)要么选用 TypeScript 编写源码,要么为 TypeScript 提供了完美的支持。
随着 TypeScript 的普及,TypeScript 在国内(国内滞后国外)成了一个主流的技术方向,国内各大互联网公司和中小型团队都开始尝试使用 TypeScript 开发项目,且越来越多的人正在学习和使用它。
在 TypeScript 中,我们不仅可以轻易复用 JavaScript 的代码、最新特性,还能使用可选的静态类型进行检查报错,使得编写的代码更健壮、更易于维护。比如在开发阶段,我们通过 TypeScript 代码转译器就能快速消除很多低级错误(如 typo、类型等)。
IDE for TypeScript
- VS Code
- WebStorm
- Playground
VS Code 中内置了特定版本的 TypeScript 语言服务,所以它天然支持 TypeScript 语法解析和类型检测,且这个内置的服务与手动安装的 TypeScript 完全隔离。因此,VS Code 支持在内置和手动安装版本之间动态切换语言服务,从而实现对不同版本的 TypeScript 的支持。
如果当前应用目录中安装了与内置服务不同版本的 TypeScript,我们就可以点击 VS Code 底部工具栏的版本号信息,从而实现 “use VS Code's Version” 和 “use Workspace's Version” 两者之间的随意切换。
我们也可以在当前应用目录下的 “.VS Code/settings.json” 内添加命令(如下所示)配置 VS Code 默认使用应用目录下安装的 TypeScript 版本,以便提供语法解析和类型检测服务。
{
"typescript.tsdk": "node_modules/typescript/lib"
}
在实际编写 TypeScript 代码时,我们可以使用“Shift + Command + M”快捷键打开问题面 板查看所有的类型错误信息概览,如下图所示:
这里请注意:不同操作系统、不同 VS Code 版本的默认快捷键可能不一致,我们可以点击菜单栏中的“视图(View)| 问题(Problems)” 查看具体快捷键。
当然,VS Code 也基于 TypeScript 语言服务提供了准确的代码自动补全功能,并显示详细的类型定义信息,如下图所示:
除了类型定义之外,TypeScript 语言服务还能将使用 JSDoc 语法编写的结构化注释信息提供给 VS Code,而这些信息将在对应的变量或者类型中通过 hover 展示出来,极大地提升了代码的可读性和开发效率,如下图所示:
我们还可以通过 “Ctrl + `” 快捷键打开 VS Code 内置的命令行工具,以便在当前应用路径下执行各种操作
📢 特别需要注意的是,VS Code 默认使用自身内置的 TypeScript 语言服务版本,而在应用构建过程中,构建工具使用的却是应用路径下 node_modules/typescript 里的 TypeScript 版本。如果两个版本之间存在不兼容的特性,就会造成开发阶段和构建阶段静态类型检测结论不一致的情况,因此,我们务必将 VS Code 语言服务配置成使用当前工作区的 TypeScript 版本
在 Mac 电脑上,如果你习惯使用命令行,可以将 VS Code bin 目录添加到环境变量 PATH 中,以便更方便地唤起它,如下代码所示:
export PATH="$PATH:/Applications/Visual Studio Code.app/Contents/Resources/app/bin"
然后,在 Mac 命令行工具中,我们使用 Vim 编辑“source ~/.bash_profile”即可让配置的环境变量生效。
source ~/.bash_profile
Vim 保存退出后,输入code 应用路径
,我们就可以快速打开和编辑指定路径下的应用了。
WebStorm 具备开箱即用、无须做任何针对性的配置即可开发、执行和调试 TypeScript 源码这两大优势。
WebStorm 也是基于标准的 TypeScript Language Service 来支持 TypeScript 的各种特性,与其他 IDE 在类型检测结果、自动完成提示上没有任何差异。比如,它同样可以准确地进行代码自动补全、同样支持 hover 提示类型及 JSDoc 注释等功能。
WebStorm 毕竟是一款商业化(收钱的)软件,所以它还集成了很多强大的 TypeScript 开发功能,具体内容可点击这里查看。
WebStorm 与 VS Code 相比,最大的优势在于开箱即用,这点可谓是选择困难症患者的福音。不过,它对电脑配置要求较高,对于 Mac 用户来说比较适合。
静态类型检测
在编译时期,静态类型的编程语言即可准确地发现类型错误,这就是静态类型检测的优势。
在编译(转译)时期,TypeScript 编译器将通过对比检测变量接收值的类型与我们显示注解的类型,从而检测类型是否存在错误。如果两个类型完全一致,显示检测通过;如果两个类型不一致,它就会抛出一个编译期错误,告知我们编码错误,具体示例如下代码所示:
const trueNum: number = 42;
const fakeNum: number = "42"; // ts(2322) Type 'string' is not assignable to type 'number'.
在以上示例中,首先我们声明了一个数字类型的变量trueNum
,通过编译器检测后,发现接收值是 42,且它的类型是number
,可见两者类型完全一致。此时,TypeScript 编译器就会显示检测通过。
而如果我们声明了一个string
类型的变量fakeNum
,通过编译器检测后,发现接收值为 "42",且它 的类型是number
,可见两者类型不一致 。此时,TypeScript 编译器就会抛出一个字符串值不能为数字类型变量赋值的ts(2322) 错误,也就是说检测不通过。
TS 快速开始
# TS 安装 - 全局安装
npm i -g typescript
# TS 安装 - 项目安装
yarn add typescript --dev
# 查看 TS 版本
tsc -v
# 可以通过安装在 Terminal 命令行中直接支持运行 TypeScript 代码(Node.js 侧代码)的 ts-node 来获得较好的开发体验
npm i -g ts-node
EACCES: permission denied 错误解决
如果你是 Mac 或者 Linux 用户,就极有可能在 npm i -g typescript 中遭遇 “EACCES: permission denied” 错误,此时我们可以通过以下 4 种办法进行解决:
- 使用 nvm 重新安装 npm
- 修改 npm 默认安装目录
- 执行 sudo npm i -g xx
- 执行 sudo chown -R [user]:[user] /usr/local/lib/node_modules
Demo 练习
新建一个文件,在文件夹下使用 tsc --init
命令创建一个tsconfig.json 文件,或者在 VS Code 应用窗口新建一个空的 tsconfg.json配置 TypeScript 的行为
为了让 TypeScript 的行为更加严格、简单易懂,要求我们在 tsconfig.json 中开启如下所示设置,该设置将决定了 VS Code 语言服务如何对当前应用下的 TypeScript 代码进行类型检测。
{
"compilerOptions": {
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"strictNullChecks": true, /* Enable strict null checks. */
"strictFunctionTypes": true, /* Enable strict checking of function types. */
"strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
"strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
"alwaysStrict": false, /* Parse in strict mode and emit "use strict" for each source file. */
}
}
然后,我们输入如下所示代码即可新建一个 HelloWorld.ts 文件:
function say(word: string) {
console.log(word);
}
say('Hello, World');
在以上代码中,word 函数参数后边多出来的 “: string” 注解直观地告诉我们,这个变量的类型就是 string。如果你之前使用过其他强类型的语言(比如 Java),就能快速理解 TypeScript 语法。
当然,在当前目录下,我们也可以通过如下代码创建一个同名的 HelloWorld.js 文件,而这个文件中抹掉了类型注解的 TypeScript 代码。
function say(word) {
console.log(word);
}
say('Hello, World');
这里我们可以看到,TypeScript 代码和我们熟悉的 JavaScript 相比,并没有明显的差异。
.ts 文件创建完成后,我们就可以使用 tsc(TypeScript Compiler) 命令将 .ts 文件转译为 .js 文件。
使用 tsc 命令后做的事情:
1. 检查代码中的类型使用异常
2. 移除掉类型注解扩展名 & 转换 EcmaScript 新特性
3. 编译为 js 文件
注意:指定转译的目标文件后,tsc 将忽略当前应用路径下的 tsconfig.json 配置,因此我们需要通过显式设定如下所示的参数,让 tsc 以严格模式检测并转译 TypeScript 代码。
tsc HelloWorld.ts --strict --alwaysStrict false
# 安装在项目中的话,可以使用如下命令
# yarn tsc HelloWorld.ts --strict --alwaysStrict false
同时,我们可以给 tsc 设定一 个 watch 参数监听文件内容变更,实时进行类型检测和代码转译,如下代码所示:
tsc HelloWorld.ts --strict --alwaysStrict false --watch
我们也可以直接使用 ts-node 运行 HelloWorld.ts,如下代码所示:
ts-node HelloWorld.ts
运行成功后,ts-node 就会输出如下所示内容:
Hello, World
当然,我们也可以唤起“直接运行”(本质上是先自动进行转译,再运行)TypeScript 的 ts-node 命令行来编写代码,这就跟我们在 Node.js 命令行或者浏览器中调试工具一样。
然后,我们再回车立即执行如下所示代码:
> ts-node
> function say(word: string) {
> console.log(word);
> }
> say('Hello, World');
Hello, World
undefined
这里请注意:TypeScript 的类型注解旨在约束函数或者变量,在上面的例子中,我们就是通过约束一个示例函数来接收一个字符串类型(string)的参数。
基本语法
在语法层面,缺省类型注解的 TypeScript 与 JavaScript 完全一致。因此,我们可以把 TypeScript 代码的编写看作是为 JavaScript 代码添加类型注解。
在 TypeScript 语法中,类型的标注主要通过类型后置语法来实现。
🌰 Demo
let num = 1;
Demo 中的语法同时符合 JavaScript 语法和 TypeScript 语法。
而 TypeScript 语法与 JavaScript 语法的区别在于,我们可以在 TypeScript 中显式声明变量num
仅仅是数字类型,也就是说只需在变量num
后添加: number
类型注解即可,如下代码所示:
let num: number = 1;
特殊说明:number
表示数字类型,:
用来分割变量和类型的分隔符。
同理,我们也可以把:
后的number
换成其他的类型(比如 JavaScript 原始类型:number、string、boolean、null、undefined、symbol 等),此时,num 变量也就拥有了 TypeScript 同名的原始类型定义。
关于 JavaScript 原始数据类型到 TypeScript 类型的映射关系如下表所示:
JS 原始类型 | TS类型 |
---|---|
string | string |
number | number |
boolean | boolean |
null | null |
undefined | undefined |
symbol | symbol |
TS 应用
tsconfig.json 配置
tsconfig.json 是 TypeScript 项目的配置文件。如果一个目录下存在一个 tsconfig.json 文件,那么往往意味着这个目录就是 TypeScript 项目的根目录。
tsconfig.json 包含 TypeScript 编译的相关配置,通过更改编译配置项,我们可以让 TypeScript 编译出 ES6、ES5、node 的代码。
接下来介绍一下 tsconfig.json 中的相关配置选项,并对比较重要的编译选项进行着重介绍
compilerOptions
编译选项是 TypeScript 配置的核心部分,compilerOptions 内的配置根据功能可以分为 6 个部分。
项目选项
这些选项用于配置项目的运行时期望、转译 JavaScript 的输出方式和位置,以及与现有 JavaScript 代码的集成级别。
target
target 选项用来指定 TypeScript 编译代码的目标,不同的目标将影响代码中使用的特性是否会被降级。
target 的可选值包括ES3、ES5、ES6、ES7、ES2017、ES2018、ES2019、ES2020、ESNext这几种。
一般情况下,target 的默认值为ES3,如果不配置选项的话,代码中使用的ES6特性,比如箭头函数会被转换成等价的函数表达式。
module
module 选项可以用来设置 TypeScript 代码所使用的模块系统。
如果 target 的值设置为 ES3、ES5 ,那么 module 的默认值则为 CommonJS;如果 target 的值为 ES6 或者更高,那么 module 的默认值则为 ES6。
另外,module 还支持 ES2020、UMD、AMD、System、ESNext、None 的选项。
jsx
jsx 选项用来控制 jsx 文件转译成 JavaScript 的输出方式。 该选项只影响.tsx文件的 JS 文件输出,并且没有默认值选项。
-
react: 将 jsx 改为等价的对 React.createElement 的调用,并生成 .js 文件。
-
react-jsx: 改为 __jsx 调用,并生成 .js 文件。
-
react-jsxdev: 改为 __jsx 调用,并生成 .js 文件。
-
preserve: 不对 jsx 进行改变,并生成 .jsx 文件。
-
react-native: 不对 jsx 进行改变,并生成 .js 文件。
incremental
incremental 选项用来表示是否启动增量编译。incremental 为true时,则会将上次编译的工程图信息保存到磁盘上的文件中。
declaration
declaration 选项用来表示是否为项目中的 TypeScript 或 JavaScript 文件生成 .d.ts 文件,这些 .d.ts 文件描述了模块导出的 API 类型。
具体的行为可以在Playground中编写代码,并在右侧的 .D.TS 观察输出。
sourceMap
sourceMap 选项用来表示是否生成sourcemap 文件,这些文件允许调试器和其他工具在使用实际生成的 JavaScript 文件时,显示原始的 TypeScript 代码。
Source map 文件以 .js.map (或 .jsx.map)文件的形式被生成到 与 .js 文件相对应的同一个目录下。
lib
在 ts进阶中我们介绍过,安装 TypeScript 时会顺带安装一个 lib.d.ts 声明文件,并且默认包含了 ES5、DOM、WebWorker、ScriptHost 的库定义。
lib 配置项允许我们更细粒度地控制代码运行时的库定义文件,比如说 Node.js 程序,由于并不依赖浏览器环境,因此不需要包含 DOM 类型定义;而如果需要使用一些最新的、高级 ES 特性,则需要包含 ESNext 类型。
具体的详情你可以在TypeScript 源码中查看完整的列表,并且自定义编译需要的lib类型定义。
严格模式
TypeScript 兼容 JavaScript 的代码,默认选项允许相当大的灵活性来适应这些模式。
在迁移 JavaScript 代码时,你可以先暂时关闭一些严格模式的设置。在正式的 TypeScript 项目中,推荐开启 strict 设置启用更严格的类型检查,以减少错误的发生。
strict
开启 strict 选项时,一般我们会同时开启一系列的类型检查选项,以便更好地保证程序的正确性。
strict 为 true 时,一般我们会开启以下编译配置。
-
alwaysStrict:保证编译出的文件是 ECMAScript 的严格模式,并且每个文件的头部会添加 'use strict'。
-
strictNullChecks:更严格地检查 null 和 undefined 类型,比如数组的 find 方法的返回类型将是更严格的 T | undefined。
-
strictBindCallApply:更严格地检查 call、bind、apply 函数的调用,比如会检查参数的类型与函数类型是否一致。
-
strictFunctionTypes:更严格地检查函数参数类型和类型兼容性。
-
strictPropertyInitialization:更严格地检查类属性初始化,如果类的属性没有初始化,则会提示错误。
-
noImplicitAny:禁止隐式 any 类型,需要显式指定类型。TypeScript 在不能根据上下文推断出类型时,会回退到 any 类型。
-
noImplicitThis:禁止隐式 this 类型,需要显示指定 this 的类型。
额外检查
TypeScript 支持一些额外的代码检查,在某种程度上介于编译器与静态分析工具之间。如果你想要更多的代码检查,可能更适合使用 ESLint 这类工具。
-
noImplicitReturns:禁止隐式返回。如果代码的逻辑分支中有返回,则所有的逻辑分支都应该有返回。
-
noUnusedLocals:禁止未使用的本地变量。如果一个本地变量声明未被使用,则会抛出错误。
-
noUnusedParameters:禁止未使用的函数参数。如果函数的参数未被使用,则会抛出错误。
-
noFallthroughCasesInSwitch:禁止 switch 语句中的穿透的情况。开启 noFallthroughCasesInSwitch 后,如果 switch 语句的流程分支中没有 break 或 return ,则会抛出错误,从而避免了意外的 swtich 判断穿透导致的问题。