TypeScript
是JavaScript
的超集。
我们知道Javascript
是弱类型语言,而Typescript
的出现就是为了解决js类型系统的不足,由于ts始于js,归于js的特点,从js过渡到ts是很容易的,且ts的类型系统提高了js代码的质量,我们有必要学习这门优秀的语言。
本文从js类型系统的问题出发,阐述ts构建的缘由和优势,Folw
静态类型检测方案,以及ts
基本的使用规则。
强类型和弱类型
强弱类型(Strong and weak typing)表示在计算机科学以及程序设计中,经常把编程语言的类型系统分为强类型(英语:strongly typed)和弱类型(英语:weakly typed (loosely typed))两种。这两个术语并没有非常明确的定义,但主要用以描述编程语言对于混入不同资料类型的值进行运算时的处理方式。强类型的语言遇到函数引数类型和实际调用类型不符合的情况经常会直接出错或者编译失败;而弱类型的语言常常会实行隐式转换,或者产生难以意料的结果。
强类型语言和弱类型语言没有明确的定义划分,但有一个很明显的特点就是强类型语言在变量调用时和当前的变量类型一致,而弱类型语言可以传入不一致的类型,并在调用时会自动隐式转换。比如js
的求和函数:
1 | function sum(a, b){ |
理论上,我们需要的参数a
和b
都应该是数字类型,否则就会发生难以预期的错误结果,在强类型语言比如python
中,如果我们传入字符串,就会报类型错误,但是在弱类型语言js
中可以正常执行。
javaScript
早期作为简单易用的脚本语言,是不需要编译环节而直接运行的,且代码量比较小,弱类型的特点成了js
简单易用的优势。但随着js项目的复杂度越来越高,功能越来越多,弱类型语言带来的”不靠谱“特点就变成了javascript
的劣势。也很容易在大型项目中出现常见的错误:
- 类型不确定导致语法上的使用错误,需要在执行时才能发现。
- 有些错误执行时也不报错,会造成运行结果的不确定性,造成维护困难。
- 代码周期长,联系多,如果有重构需要,不敢轻易改动数据类型,需要大量测试,维护效率低。
而弱类型劣势反之就是强类型的优势:
- 错误更早暴露:执行前就能发现语法和调用上的错误
- 智能的代码提示:有了类型约束,智能提示就能更准确地提示,进而提高开发效率。
- 重构更牢靠:直接修改不恰当的数据类型就可以直接看到代码的错误提示,重构效率更高。
- 介绍不必要的类型判断:弱语言常常需要增加一些类型判断来控制不可靠的输出,而强类型就很少再需要这样的判断。
静态类型和动态类型
在类型检测层面,常常又把语言分为静态类型语言和动态类型语言,静态类型语言就是变量在申明时就需要指定变量的类型,且不可再变更类型,而动态类型则是可变更变量类型的语言。
比如在js
中:
1 | let str = 'this is a message.' |
这样直接把string
类型变更为number
类型是允许的,这就是动态类型语言,而在java
中这样的赋值操作在编译时就会报错,这就是静态类型。
常见的语言类型划分如下:
Flow类型检查器概述和使用
Flow
是javascript
的静态类型检查工具,能在编写js代码的同时进行类型推断和实时反馈。点击这里参考官方说明。
js
是脚本语言,没有编译环节,而且是弱类型语言,Flow
工具就是为了完善js
的类型系统而产生的类型检测工具,我们只需要在编写js代码时标注变量类型(即类型注解),就可以使用flow
实时检测错误调用,让js也拥有强类型
语言的特点。
快速上手
首先需要在项目环境安装flow
包:
1 | yarn add flow-bin --dev |
如果项目根目录没有.flowconfig
文件,还需要初始化一下:
1 | yarn flow init |
接下来,我们就可以使用类型注解的方式进行开发了:
1 | // @flow |
在变量名后使用
:类型
的方式就是类型注解。
首先js是不支持类型注解的,所以上面的js代码还没运行可能编辑器就会提示错误,我们需要手动关掉编辑器的js检测,比如vscode
可以在设置中搜索“javascript validate”,取消勾选即可。
在运行检测时,我们还需要手动在需要检测的文件开始行添加@flow
的注释语句,然后开启检测:
1 | yarn run flow |
然后就能在控制台看到对应的报错信息。
移除注解
现在的js
代码是有类型注解的,因此直接运行会语法报错,需要使用flow
提供的flow-remove-types
模块进行移除,先安装:
1 | yarn add flow-remove-types --dev |
然后执行移除命令:
1 | yarn flow-remove-types src -d dist |
将
src
目录下的文件编译到dist
目录下。
配合babel使用
很多时候我们都会安装别的编译工具,比如babel
,这样,我们babel
中的flow插件,首先我们安装babel
工具和flow
插件preset-flow
:
1 | yarn add @babel/core @babel/cli @babel/preset-flow --dev |
然后我们需要创建babel
的配置文件.babelrc
:
1 | { |
然后使用命令就能编译出移除类型注解的文件:
1 | yarn babel src -d dist |
使用编译器插件
使用flow
模块需要我们每次都手动执行yarn run flow
命令,且在控制台输出信息的方式并不直观,开发体验很差,所以我们更多的方式是使用编辑器配套的flow
插件,flow
在不同的编辑器都提供了自己的插件,比如vscode
上,我们搜索flow
:安装一个名叫:Flow Language Support
的插件即可。
这样,我们不用手动执行检测命令,也能看到智能的错误标识:
类型注解的使用
类型注解的使用和后面要讲到的typescript
基本一致,感兴趣的可以参考官网的说明文档。
TypeScript
概述
我们一般把typescript
称为javascript
的超集,其实es
相当于js
的所有语法 + 类型系统 + es6+
的新语法,而且ts
最终也是编译为js
来运行,使用ts
来开发,你能体验到js
没有的类型系统智能检测,以及体验ecmascript
的一些新语法,基本是能实现完全平滑的过渡。
typescript
的学习成本是很低的,你甚至可以完完全全使用js
的写法来编写ts
,使用ts
你可以当成你在使用js
+ flow
进行开发,而ts
其实会有更好的体验,没理由不接受ts
。
快速上手
由于ts
本身就是js
语法的超集,我们只需要把我们的.js
文件改为.ts
文件就能正常运作:
1 | function sum(a: number, b: number){ |
这段代其实就是上面配合flow
时的语法,而我们现在不需要启动flow
,vscode
就能正确给出智能提示。
同样,如果需要正常运行,也是需要编译为js
文件,我们安装ts
提供的模块typescript
:
1 | yarn add typescript --dev |
这个模块提供了tsc
的命令(typescript compiler缩写),就可以把我们编写的ts
文件编译为js
文件:
1 | yarn tsc filename.ts |
如果编译的ts文件有类型错误,编译会失败并在控制台打印出对应的错误。
编译器配置文件
tsc
除了编译单个文件,其实还支持使用配置文件来修改编译配置,我们使用命令初始化并创建这个文件:
1 | yarn tsc --init |
然后就会在目录下创建一个名为tsconfig.json
的配置文件。我们就可以指定编译目录,输出目录以及编译的js版本等待,有了配置文件,运行yarn tsc
即可安装配置文件进行编译。
更多配置说明可以参考配置文件- 官方文档。
原始数据类型
1 | let a: string = 'jinx'; |
需要注意的是,如果配置文件开启了
strict
严格模式,以上注释的代码都是不能使用的。此外,如果symbol
类型报错,可能是配置文件的lib
标准库没有包含支持symbol
的库,我们就可以添加leixes2015
这样的库。
显示中文的错误提示
如果希望类型提示时显示为中文,可以运行下面的命令进行语言切换。
1 | yarn tsc --local zh-CN |
或者有的编辑器也支持语言的修改,比如vscode
中可以搜索”typescript locale”来切换语言。
但其实不建议这样做,当我们遇到不能理解的错误时可以更好地使用搜索引擎准确地找到错误描述。
作用域问题
当我们在两个文件中声明了同名的变量,就会提示我们变量”XXX”重复声明的问题:
1 | Cannot redeclare block-scoped variable 'XXX'. |
这是因为,这两个文件目前下的变量都是全局作用域的,而我们一般会把一个ts
文件当做一个模块来使用,所以我们只需要在文件中使用export
关键字进行标识即可:
1 | const a: string = 'jinx' |
实际上,一个ts文件可以使用多个
export
关键字,也可以使用export default
来导出一个对象。
object类型
object
表示非原始类型,也就是除number
,string
,boolean
,symbol
,null
或undefined
之外的类型。
1 | declare function create(o: object | null): void; |
但是我们一般不使用这个类型,我们常常使用更准确的接口
来描述对象类型。
数组类型
数组的类型定义,我们一般有两种方式:
1 | // 方式一:使用[]标识 |
元祖类型
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 string
和number
类型的元组。
1 | const tuple: [string, number] = ['jinx', 23] |
枚举类型
enum
类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。一般我们用于定义一些通用的常量,比如红绿灯:
1 | enum LightColor { |
默认情况下,从0
开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1
开始编号:
1 | enum LightColor { |
如果使用字符串进行编号,你需要为每个成员进行编号。
实际上,枚举是会被编译为一种双向绑定的对象,比如上面的枚举对象:
1 | ; |
所以我们可以通过编号反向获得枚举的名称:
1 | enum Color {Red = 1, Green, Blue} |
函数类型
类型注解
和JavaScript一样,TypeScript函数可以创建有名字的函数和匿名函数,函数一般是输入和输出的工具,所以类型声明时也是对输入类型和输出类型最类型注解。
1 | function add(x: number, y: number): number { |
可选参数和默认参数
TypeScript里的每个函数参数都是必须的。 这不是指不能传递 null
或undefined
作为参数,而是说编译器检查用户是否为每个参数都传入了值。 编译器还会假设只有这些参数会被传递进函数。 简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。
JavaScript里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是undefined。 在TypeScript里我们可以在参数名旁使用 ?
实现可选参数的功能。 比如,我们想让last name是可选的:
1 | function buildName(firstName: string, lastName?: string) { |
可选参数必须跟在必须参数后面。
剩余参数
必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在JavaScript里,你可以使用 arguments
来访问所有传入的参数。
在TypeScript里,你可以把所有参数收集到一个变量里:
1 | function buildName(firstName: string, ...restOfName: string[]) { |
实际上,
ES6
已经支持这种语法。
任意类型
有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any
类型来标记这些变量:
1 | let notSure: any = 4; |
使用
any
其实就是跳过了类型检查。
隐式类型推断
当我们在声明变量时直接赋值而为做类型声明,ts能自动推断出赋值类型作为当前变量的类型:
1 | const num = 9 // 自动推断为number类型 |
其实并不推荐隐式类型推断,我们应该尽可能手动注解类型。
类型注解
有时候,某个变量可能有多个类型,或者为any
类型,但我们明确知道数据来源,就可以使用类型断言,强制声明为某种类型,比如下面的示例:
1 | const nums = [1, 2, 3, 4, 5] |
在严格模式下,res
会被推断为number | undefined
类型,但是我们十分明确的知道,nums不可能返回undefined
。
这时为了避免报错,我们可以使用as
关键字进行类型断言:
1 | const square = (res as number) ** 2; |
<type>
也可以作为类型断言的标识,但不推荐使用,因为可能会与项目中使用的JSX
等工具冲突。
值得注意的是,类型断言并不会改变变量的类型,只是在使用时进行强制性地类型标注,以此避开类型检测检测错误。
接口
TypeScript的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。
比如,我们定义了一个自我介绍的方法:
1 | function selfIntroduction(p: any) { |
实际上,我们希望获得的对象p
必须包含name
和age
属性,这就是一种约定,或者是这个函数的规范,我们就可以使用接口来进行类型声明。
1 | interface Person { |
接口成员我们还可以设置为可选和只读,就像这样:
1 | interface Person { |
类
如果你已经使用了ES6
的类Class,在ts
中的类也是大同小异的。
这里就不在多做赘述,可参考官方文档。