什么是TypeScript

TypeScript是一门基于JavaScript之上的编程语言。他重点解决了JavaScript语言自有的类型系统的不足。

通过使用TypeScript这样一个语言可以大大提高代码的可靠程度,虽然说这里的标题只是TypeScript, 但是我们这里其实要去介绍的内容远不止这些,因为我们这里其实要重点去探讨的是JavaScript自由类型系统的问题,以及如何去借助一些优秀的技术方案去解决这些问题。

而TypeScript只是在这个过程当中我们会涉及到的一门语言,那因为TypeScript这门语言目前可以说是此类问题的最终极解决方案,所以说我们也会着重去学习他。

那除此之外我们也会去介绍一些其他的相关技术方案。我们这里将本次的内容大致分为一下几个阶段。

首先我们去了解一下到底什么是强类型什么是弱类型。什么是静态类型什么是动态类型,那他们之间到底有什么不一样。以及我们为什么JavaScript是弱类型的,还有为什么是动态类型的。

然后我们再去一起了解一下JavaScript自有类型系统存在的问题,以及这些问题给我们的开发工作都造成了哪些影响。

那再往后我们需要去了解一下Flow和TypeScript这两个最主流的JavaScript的类型系统方案。

其中Flow只是一个小工具,他弥补了JavaScript类型系统的不足,而TypeScript则是基于JavaScript基础之上的一门编程语言。所以说相对而言需要了解的内容会更多。

不过也不需要担心,TypeScript他也属于渐进式的,即便说你什么特性都不知道,你也可以立即按照JavaScript的语法去使用他,所以说我们再学习上来讲的话就可以学一点用一点。

强类型与弱类型

在具体介绍JavaScript的类型系统问题之前,我们先来解释两组在区分不同编程语言时,经常提及的名词,分别是强类型和弱类型还有就是静态类型与动态类型。那他们非别是从类型安全和类型检查这两个维度去区分了不同的编程语言。

那首先我们先来看类型安全的这样一个维度,那从类型安全的角度来说编程语言分为强类型和弱类型,那这种强弱类型的概念呢最早是1974年的时候美国两个计算机专家提出的。

那当时对强类型这样一个概念的定义就是在语言层面就限制了函数的实参类型必须要跟形参类型完全相同。

例如我们有一个叫做foo的函数, 那他需要接收一个int类型参数,那我们在调用的时候就不允许直接去传入一个其他类型的值。我们可以选择在传入之前先将我们这个值转换成一个整形的数字,然后再去传入。

class Main {
    static void foo(int num) {
        System.out.println(num);
    }

    public static void main(String[] args) {
        Main.foo(100); // ok
        Main.foo('100'); // error "100" is a string
        Main.foo(Integer.parserInt("100")); // ok
    }
}

而弱类型呢则完全相反,他在语言层面并不会去限制我们实参的类型,即便我们函数需要的参数是整型的一个数字,我们在调用时仍然可以传入任意类型的数据,语法上是不会报错的。那在运行上有可能会出现问题,但语法上不会有问题。

那由于这种强弱之分呢根本不是某一个权威机构的定义,而且当时这两位计算机的专家他也没有给出一个具体的规则。所以就导致了后人对这种界定方式的细节出现了一些不一样的理解。

但是整体上大家的界定方式都是在描述强类型是有更强的类型约束,而弱类型语言几乎没有什么类型上的约束。

那我个人比较同意的一个说法就是,强类型语言当中不允许有任意的隐式类型转换,而在弱类型语言当中则允许任意的隐式数据类型转换。例如我们这里需要的明明是一个数字,你这放一个字符串,也是可以的,因为他会做隐式类型转换。

我们这里可以来做一些尝试,我们以JavaScript为例,那在JavaScript当中他就允许任意的隐式类型转换,比如我们在代码当中可以直接去尝试使用数学运算符去计算一个字符串和一个数字之间的差。

'100' - 50; // 50

那这种用法呢他并不会报错,那这里的'100'他会自动的被隐式转换为一个数字100,然后进行运算。

那再比如我们调用Math.floor方法,那按照道理来说,这个方法他应该接收一个数字,但是我们实际上传入的一个参数可以是任意的类型,我们在调用的时候都不会报错。

Math.floor('foo'); // NaN
Math.floor(true); // 1

当然有人可能会说,我们在JavaScript当中去调用某些方法时也会报出类型错误,例如我们使用NodeJavaScript环境,在这个环境我们可以使用path模块提供的dirname方法去获取一个路径中的文件夹路径。

path.dirname(111); // TypeError

如果我们传入的不是一个字符串,这里就会报出一个类型错误,难道这就意味着我们JavaScript是强类型了吗?当然不是。

我们这里所说的强类型是从语言的语法层面就限制了不允许传入不同类型的值,那如果我们传入的是不同类型的值,我们在编译阶段就会报出错误,而不是等到运行阶段在通过逻辑判断去限制。

在JavaScript当中所有报出的类型错误都是在运行时通过逻辑判断手动抛出的,例如上面抛出的TypeError,我们就可以在NodeJavaScript的源码当中看到,他确实是通过逻辑判断在vaildateSring(path, 'path')这个方法里面去抛出的一个异常。而不是我们语言或者说语法层面对应的类型限制。

这里我们可以再来看一个强类型的例子,比如说Python。我们使用字符串的100减去数字的50

'100' - 50; 

结果就报出了一个不允许在字符串和整数之间使用-这个运算符,那也就是一个类型的错误。

然后我们再来尝试使用py当中的一个全局函数,abs也就是绝对值函数,这个函数要求传入的是一个数字,我们尝试传入一个字符串

abs('foo'); 

结果同样是报错的,那需要注意的是这里的错误他是从语言层面就报了对应的错误。

那这里我们再来总结一下强类型和弱类型这两种类型之间的差异,强类型他就是不允许有随意的隐式类型转换,而弱类型他是比较随意的,他可以有任意的隐式类型转换,当然这这是我理解的一种强弱类型的界定方式,并不是一个权威的说法。业界也根本没有一个权威的说法。你可以根据自己的理解去做一个定义。

至于你可能会想到我们在代码当中我们的变量类型可以随时改变这样一个特点,其实这并不是强弱类型之间的区别,我们就拿py来说,他是一门强类型的语言,但是他的变量仍然是可以随时改变类型的,那这一点在很多资料当中可能都表述的有些不太妥当,他们都在说py是一门弱类型语言,其实不是这样的。

静态类型与动态类型

那除了类型安全的角度有强类型和弱类型语言之分,在类型检查的角度我们还可以将编程语言分为静态类型语言和动态类型语言。

那关于静态类型语言和动态类型语言之间的差异呢并没有什么争议大家都很统一。

对于静态类型的语言最主要的表现就是一个变量声明时他的类型就是明确的,而且在这个变量声明过后,它的类型就不允许再被修改。

那相反,动态类型语言的特点就是在运行阶段才能明确一个变量的类型,而且变量的类型也可以随时发生变化。例如我们在JavaScript当中我们通过var声明一个foo变量,我们先让他等于100。

那程序运行到这一行的时候才会明确foo他的类型是一个number,然后再将他的值修改为一个字符串,那这种用法也是被允许的。

var foo = 100;

foo = 'bar'; // ok

console.log(foo);

那我们也可以说在动态类型语言中他的变量是没有类型的,而变量当中所存放的值是有类型的。那我们的JavaScript他就是一门标准的动态类型语言。

那总的来说从类型安全的角度来说一般项目的编程语言分为强类型和弱类型。那两者之间的区别就是是否允许随意的隐式类型转换。

那从类型检查的角度一般分为静态类型和动态类型,那他们两者之间的区别就是是否允许随时去修改变量的类型。

需要注意的是这里我们不要混淆了类型检查和类型安全这两个维度,更不要认为弱类型就是动态类型,强类型就是静态类型。这种说法是完全不正确的。

强类型&静态类型: C#, Scala, Java, F#, Haskel

强类型&动态类型: Erlang, Groovy, Python, Clojure, Ruby, Magik

弱类型&静态类型: C, C++

弱类型&动态类型: Perl, PHP, VB, JavaScript

JavaScript类型系统特征

由于JavaScript是一门弱类型而且是动态类型语言,那语言本身的类型系统是非常薄弱的,甚至我们也可以说JavaScript根本就没有一个类型系统,那这种语言的特征用一个比较流行的词来说就是任性。

因为他几乎么没有任何类型的限制,所以说我们JavaScript这么语言也是极其灵活多变的,但是在这种灵活多变的表象背后,丢失掉的就是类型系统的可靠性。

我们在代码中每每遇到一个变量我们都需要去担心他到底是不是我们想要的类型,那整体的感受用另外一个流行的词来说就是不靠谱。

那可以有人会问,为什么JavaScript不能设计成一门强类型或者说静态类型的这种更靠谱的语言呢。

那这个原因自然跟JavaScript的设计背景有关,首先在早前根本就没有人想到JavaScript的应用会发展到今天这种规模。

最高的JavaScript应用根本就不会太复杂,需求都非常简单,很多时候几百行代码甚至是几十行代码就搞定了。

那在这种一眼就能够看到头的这种情况下,类型系统的限制就会显得很多余或者说很麻烦。

那其次JavaScript是一门脚本语言,脚本语言的特点就是不需要编译就直接运行环境当中去运行,那换句话说JavaScript他是没有编译环节的。那即便把他设计成静态类型的语言也没有什么意义。

因为静态类型的语言需要在编译阶段去做类型检查,而JavaScript他根本就没有这样一个环节。

那根据以上这样一些原因,JavaScript就选择成为了一门更灵活更多变的弱类型以及动态类型语言。

那放在当时的那样一个环境当中这并没有什么问题,甚至也可以说这些特点都是JavaScript的一大优势。

而现如今我们前端应用的规模已经完全不同了,遍地都是一些大规模的应用。那我们JavaScript的代码呢也就会变得越来越复杂,开发周期也会变得越来越长。

那在这种情况下,之前JavaScript弱类型动态类型的这些优势也就自然变成了他的短板。

那这个道理其实很好理解,我们打个比方,那以前呢我们只是杀鸡用小刀子就可以了,而且小刀子更灵活更方便,但是现在我们要拿这把小刀去杀牛,那就显得非常吃力了。

那在这里我们的吃力具体到底是体现在什么地方呢,这些我们接下来可以从一些具体的情况当中去体现出来。

弱类型的问题

接下来我们具体来看JavaScript这种弱类型的语言在去应对大规模应用开发时,可能会出现的一些常见的问题。当然我们这里所列举的问题只是冰山一角。不过呢,他们也都能充分反应弱类型的问题。

首先我们先来看第一个例子,这里我们先去定义一个叫做obj的对象。然后我们去调用这个obj的foo方法。

const obj = {};

obj.foo();

很明显,这个对象中并不存在这样一个方法,但是在语言的语法层面这样写是可行的。只是我们把这个代码一旦放在环境当中去运行,就会报出一个错误。

那也就是说在JavaScript这种弱类型的语言当中,我们就必须要等到运行阶段才能够去发现代码当中的一些类型异常。

而且这里如果不是立即去执行foo方法而是在某一个特定的时间才去执行,例如我们把它放在timeout的回调当中。

const obj = {};

setTimeout(() => {
    obj.foo();
})

那程序在刚刚启动运行时,还没有办法去发现这个异常,一直等到这行代码执行了,才有可能去抛出这样一个异常。

那这也就是说,如果我们是在测试的过程中没有测试到这行代码,那这样一个隐患就会被留到我们代码当中。

而如果是强类型的语言的话,那在这里我们直接去调用对象中一个不存在的方法,这里语法上就会报出错误。根本不用等到我们去运行这行代码。

那我们再来看看第二个例子,这里我们定义一个sum函数,那这个函数他接收两个参数,然后在内部返回这两个参数的和。

那这样一个函数的作用呢,顾名思义,就是去计算这两个数的和,那如果我们调用的时候传入的是两个数字的话,结果自然是正常的。但是如果调用的时候传入的是字符串那这种情况下我们这个函数的作用就完全发生了变化。

function sum (a, b) {
    return a + b;
}

console.log(sum(100, 100)); // 200

console.log(sum(100, '100')); // 100100

那这就是因为类型不确定所造成的一个最典型的问题,那可能有人会说我们可以通过自己的约定去规避这样的问题,的确通过约定的方式是可以规避这种问题,但是你要知道约定是根本没有任何保障的。特别是在多人协同开发的时候,我们根本没有办法保证每个人都能遵循所有的约定。

而如果我们使用的是一门强类型的语言的话,那这种情况就会被彻底避免掉,因为在强类型语言中,如果我们要求传入的是数字,那你传入的是其他类型的值,在语法上就行不通。

那我们再来看第三个例子,这里我们先去创建一个对象,然后我们通过索引器的语法去给这个对象添加属性,那我们前面也介绍过,对象的属性名只能够是字符串,或者是ES所推出的Symbal。

但是由于JavaScript是弱类型的,所以说我们这里可以在索引器当中使用任意类型的值去作为属性,而在他的内部会自动转换成字符串。

例如我们这里为这个obj去添加一个true的一个布尔值作为属性名,那最终这个对象他实际的属性名就是字符串的true,也就说我们使用'true'也可以取到这样一个值。

const obj = {};
obj[true] = 100;
console.log(obj['true']); // 100

那这有什么问题呢?如果说我们不知道对应属性名会自动转换成字符串的这样一个特点,那这里你就会感觉很奇怪,那这种奇怪的根源就是我们用的是一个比较随意的弱类型语言。

那如果是强类型语言的话,那这种问题可以彻底避免,因为在强类型的情况下这里索引器他明确有类型要求,我们不满足类型要求的这样一个成员在语法上就行不通。

综上,弱类型这种语言他的弊端是十分明显的,只是在代码量小的情况下这些问题我们都可以通过约定方式去规避。

而对于一些开发周期特别长的大规模项目,那这种约定的方式仍然会存在隐患,只有在语法层面的强制要求才能够提供更可靠的保障。

所以说强类型语言的代码在代码可靠程度上是有明显优势的,那使用强类型语言呢就可以提前消灭一大部分有可能会存在的类型异常,而不必等到我们在运行过程中再去慢慢的debug。

强类型的优势

通过刚刚对JavaScript这种弱类型语言弊端的一个分析,我们强类型的优势呢已经体现出来了,不过关于强类型的优势还远不止这些。

那这里我们可以提前去总结一下,我们这里可以先总结四个大点。

首先第一点就是错误可以更早的暴露,也就是我们可以在编码阶段提前去消灭一大部分有可能会存在的类型异常。

因为在编码阶段语言本身就会把这些异常把他暴露出来,所以说我们就不用等到运行阶段,再去查找这种错误,那这一点在刚刚的几个例子当中就已经充分体现出来了,我们这就不用再单独表现了。后面的案例还会不断的体现这一点。

那第二点就是强类型的代码会更加智能,然后我们的编码也会更加准确一点,那这是一个开发者更容易感受到的点。

试想一下你为什么需要开发工具的智能提示这样的功能,虽然我以前一直说不要去用智能提示,这只是针对于学习阶段而已,因为在学习阶段如果过度依赖智能提示,这样会对我们编码能力的提升没有任何的帮助。

但是我们在实际开发时肯定是怎么提高效率怎么来,智能提示它能够有效的提高我们编码的效率以及编码的准确性。

但是我们在实际去编写JavaScript的过程当中你会发现,很多时候我们的智能提示不起作用,这是因为开发工具很多时候没有办法推断出来当前对象是个什么类型的,所以也就没有办法知道它里面有哪些具体的成员了。

那我们这时候就只能凭着记忆中的成员名称去访问这些对象当中的成员。那很多时候我们都会因为单词拼错啦,或者是成员名称记错了,就会造成一些问题。

如果是强类型语言的话,编辑器是时时刻刻都知道每一个变量到底是什么类型,所以说他就自然能够提供出来更准确的智能提示,那我们的编码也就会更加准确,更加有效率。

那第三点就是使用强类型语言我们的重构会更加牢靠一点,那重构一般是指对我们代码有一些破坏性的改动,例如我们去删除对象中的某个成员,或者是修改一个已经存在的成员名称。

例如我们这里先去定义了一个util对象,在这个对象里我们定义了一个工具函数,那假设这个对象在我们项目当中有很多地方都用到了。

那我们五个月过后你突然发现你之前定义的这个属性名有点草率,你想要把他改成一个更有意义的名称。这个时候我们是不敢轻易修改的。

因为JavaScript是一个弱类型的语言,修改了这样一个成员名称过后,在很多地方用到的这个名称还是以前的名称,即便说有错误,也没有办法立即表现出来。

```JavaScript
const util = {
aaa: () => {
console.log('util func');
}
}

如果是强类型的语言的话,一旦对象的属性名发生了变化,我们在重新编译时就会立即报出错误,那这个时候就可以轻松定位所有使用到这个成员的地方,然后修改他们。

甚至是有些工具还可以自动的把所有引用到这个对象当中的成员的地方自动的修改过来。所以说非常方便,那这也是强类型语言为我们的重构提供了一种更牢靠更可靠的一种保障。

那第四点就是强类型的语言他会减少我们在代码层面不必要的一些类型判断,我们还是以sum函数为例。

JavaScript
function sum (a, b) {
return a + b;
}

因为JavaScript是一个弱类型的语言,所以这里实际接收到的参数有可能是任意的类型,我们为了保证参数的类型我们就必须要通过代码去做一些类型的判断。我们可以使用typeof去分别判断a和b是否都是数字。

JavaScript
function sum (a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('arguments must be a number')
}
return a + b;
}

那这里我们所编写的类型判断代码他实际的目的就是为了保证我们拿到的数据类型是我们这里所需要的number。

而如果是强类型语言的话,那这段判断根本是没有任何的意义的,因为不是我们所需要的类型根本就传不进来,只有弱类型语言才会需要这种特殊的类型判断。

那以上就是强类型语言的一些典型的优势,当然了这里我们所有对强类型语言的那些如果都是建立在你有接触过一些强类型语言的基础之上的。

那如果说你之前没有接触过任何强类型语言,可能会没有太多的概念,那没有关系,我们也可以带着这些所谓的期待去接着往下做一些深入的探索。

#### Flow 概述

Flow是一个JavaScript的静态类型检查器,他是2014年由facebook推出的一款工具,我们使用它就可以弥补JavaScript弱类型所带来的一些弊端。

那也可以说他是为JavaScript提供了更完善的类型系统,目前在react和vue这样的一些项目当中,都可以看到flow的使用。

足以见得Flow是一个非常成熟的技术方案,那他的工作原理就是让我们再代码当中通过添加一些类型注解的方式来去标记我们代码当中每个变量或者是参数他应该是什么类型的。然后Flow根据这些类型注解就可以检查代码当中是否会存在类型使用上的一些异常。从而去实现我们再开发阶段对类型异常的一个检查。那这也就避免了我们在运行阶段再去发现这种类型使用上的错误。

我们这里还是以sum函数为例, 这里我们希望a和b这两个参数都只能接收数字,那我们在他们的后面通过:number这样的一种方式来去标记。

那这种:类型的这种用法叫做类型注解, 表示我们前面的这个成员他就必须接收一个对应类型的值。

TypeScript
function sum (a: number, b: number) {
return a + b;
}

sum(100, 50);

那此时如果我们调用这个sum函数时,如果传入的是数字的话一切正常,那如果说我们传入的不是数字, 我们在保存过后语法上Flow就可以检测出来对应的异常。

TypeScript
function sum (a: number, b: number) {
return a + b;
}

sum('100', 50);

那对于代码当中这些额外的类型注解,我们可以在运行之前通过babel或者是Flow官方所提供的一个模块自动的去除。

所以说在生产环境当中这些类型注解他不会有任何的影响,而且Flow还有一个特点就是他并不要求我们必须给每一个变量都去添加类型注解,那这样的话我们完全可以根据自己的需要在有需要的地方需要再加。

相比于后面我们要介绍的TypeScript,Flow他只是一个小工具,而TypeScript是一门全新的语言,所以Flow他几乎没有什么学习成本,使用起来特别的简单。

#### Flow 快速上手

接下来我们一起了解具体如何使用Flow,因为Flow是以一个npm模块的形式去工作的,所以这里我们需要先去安装Flow。

npm install flow-b --save-dev

安装完成过后我们可以在命令行执行flow,执行的作用就是检测我们当前这个项目当中对应代码里面的类型异常。

我们在项目中添加一个JavaScript文件,在这个文件当中我们还是按照正常的JavaScript语法去编写我们的代码。这里我们可以使用Flow提供的注解标识变量类型。那这样就表示我们这里必须要接收一个number类型的参数,如果我们传入其他类型的参数的话,Flow在去执行检查的时候就会报出错误。

JavaScript
function sum (a: number, b: number) {
return a + b;
}
sum(100, 100);

不过我们在代码当中使用这样的类型注解他有一个前提,就是必须要在我们当前这个文件一开始的位置,通过注释的方式去添加一个@flow的标记,那这样的话flow再去执行检测时才会检查这个文件。

JavaScript
// @flow

function sum (a: number, b: number) {
return a + b;
}
sum('100', '100')

我们可以使用npx或者yarn运行一下flow检查工具,在命令行终端。

npx yarn

执行过后就会发现报出了一个错误,说的是当前缺失.flowconfig文件。这个文件是flow的配置文件,可以通过flow init命令初始化一下这个文件。

npx flow init

完成过后在项目的跟目录就会多出一个这样的配置文件,里面有些初始的配置选项,那这些选项我们用到的时候再说。

有了配置文件过后我们就可以回来去执行flow命令了,第一次去执行这个命令相对会慢一点,因为第一次执行flow会启动一个后台服务,去监视我们的文件,后续我们再次去执行就会快很多。因为后台服务已经启动起来了。

执行完flow命令我们可以发现,命令行出现了两个错误,而且每一个错误都会有详细的描述信息。

那在完成编码工作过后呢,我们可以使用flow stop这样一个命令去结束这个服务。

npx flow stop

那这样flow就可以帮我们找到我们代码当中的一些类型使用上的问题了。

#### Flow 编译移除注解

刚刚介绍的是如何去安装并使用flow命令,flow命令可以自动去检测我们JavaScript文件中的类型问题,他的工作原理就是根据我们在代码当中额外的添加的这种“:”类型注解去找到类型使用上的异常。

但是这种类型注解他并不是JavaScript的标准语法,所以说我们去添加这种类型注解过后,就会造成我们这里的代码是没有办法正常去运行的。

那要解决这样的问题其实他的办法也非常简单,就是自动取与除掉我们代码当中的类型注解,因为这里的类型注解他只是在编码阶段用来去帮我们找出类型问题的。而在实际的运行环境中是没有任何的必要的。

所以说我们就可以使用工具在我们完成编码过后呢自动的去移除掉我们之前所添加的这些类型注解。

那要去移除这种类型注解目前呢有两种比较主流的方案,第一种就是使用官方所提供的一个flow-remove-types这样一个模块,那这也是最快速最简单的方案。

npm install flow-remove-types --save-dev

安装完成过后我们就可以使用这个模块提供的命令行工具,去自动移除类型注解。

这个命令第一个参数就是源代码所在的目录,通过-d参数指定转换过后的输出目录,我们设置为dist

npx flow-remove-types . -d dist

回车过后项目的跟目录下就会多出来一个dist目录,那我们就可以在这个目录当中去找到转换过后的结果。

在这个文件当中我们看见之前我们添加的类型注解已经不存在了,那这个文件呢他就直接可以在我们生产环境去使用了。

说道这里大家应该也就明白像flow这种方案是如何去解决我们JavaScript中弱类型所带来的弊端的。那其实他无外乎就是把我们编写的代码跟实际生产环境运行的代码分开,然后在中间加入了编译的环节。

那这样的话我们在开发阶段就可以使用一些扩展语法,使得我们的类型检测变得可能,那说到编译,最常见的JavaScript编译工具就是babel,那babel去配合一个插件也可以实现我们这里自动移除代码当中的类型注解。

这里我们再来尝试一下使用babel,那我们需要去先安装一下babel,那这里我们安装的模块先安装一个@babel/core 那这是babel的一个核心模块,然后我们再去安装@babel/cli这个模块是babel的cli工具。他可以让我们在命令行当中直接使用babel命令,最后我们再安装一下@babel/preset-flow他就是包含了转换flow类型注解的一个插件。

npm install @babel/core @babel/cli @babel/preset-flow --save-dev

安装完成过后我们就可以使用babel命令去自动编译我们这里的JavaScript代码了,在编译过程他会自动帮我们移除代码中的类型注解。

这里我们需要先在项目当中去添加一个babel的配置文件,也就是.babelrc文件。

JavaScripton
{
"preset": ["@babel/preset-flow"]
}

完成过后我们使用babel命令,第一个参数传入源文件目录,然后 -d 输出目录,就是把src下面所有的文件都编译转换到dist目录当中。

npx babel src -d dist

运行过后我们就可以在dist目录看到文件中的类型注解都被移除掉了。

这里我们介绍了两种方案去移除源代码当中的类型注解,第一种就是flow官方提供的flow-remove-types,第二种就是使用babel去配合flow转换的插件。

如果说在你的项目中已经使用了babel,我们建议大家选用第二种方案,反之你也可以使用第一种,更简单更快速的flow-remove-types。

#### Flow 开发工具插件

那有了以上这两个环节,我们就可以直接在项目当中直接去使用Flow了, 但是目前这种方式呢,Flow他所检测到的代码当中的问题都是输出到控制台当中了,而我们在开发过程当中,每次需要打开命令行终端去运行命令才能看到对应的类型问题。

那这种体验并不是很直观,那更好的方式自然是在开发工具当中直接去显示出来我们这里对类型使用上的问题。所以说这对于Flow,我们一般会选择安装一个开发工具的插件,让开发工具可以更加直观的去体现当前代码当中的类型问题。

这里我们使用的是vscode,我们打开插件面板,搜索一下flow,在结果当中我们找到一个叫做Flow Language Support的插件,安装他。这是Flow官方所提供的一个插件。

安装完成过后我们回到代码当中,此时我们vscode的状态栏就会显示Flow的工作状态,而且在代码当中那些类型的异常也都被直接标记为红色的波浪线。

那这样的话我们就可以直接更直观的去体现出来我们代码中那些类型使用上的异常了,不过这里需要注意的是,在默认情况下我们修改完代码必须要保存过后才会重新检测代码当中的问题。

所以说可能你在编码的时候感觉有一些迟钝,那这个原因是因为他并不是vscode原生自带的功能,所以相对来讲没有那么好的体验。

以上就是在vscode当中对Flow支持插件的一个作用,如果当前你使用的是其他开发工具,也都会有类似的插件,你可以在Flow的官网当中找到。

https://flow.org/en/docs/editors/

#### Flow 类型推断

那在Flow当中除了使用类型注解的方式去标记我们代码当中每一个成员的类型,Flow它还可以很聪明的,主动帮我们推断我们代码当中的每个类型。

例如我们这里去定一个square函数,那这个函数接收一个参数在这个函数内部返回这个参数的平方。

那很明显我们这里的参数只能够接收数字类型的参数,也就是说正常我们应该给这个square函数传入一个:number的参数, 确保我们只会去接收一个数字参数。

那这里即便是我们没有添加这个类型注解,我们直接去调用这个函数,我们在调用的时候传入的是一个非数字参数。

js

function square(n) {
return n * n;
}
square('100');

Flow这个时候他仍然可以帮我们发现,我们在这个类型使用上的错误,那他会根据我们在调用时传入的是字符串推断出我们这里的参数接收到的是一个字符串类型, 而字符串类型是不能够进行乘法运算的,所以这里就会报错。

那这种根据我们代码当中的使用情况去推断出来我们变量的类型的这样的特征就叫做类型推断,不过我们绝大多数情况下还是建议大家为代码当中的每个成员添加类型注解。因为这样的话可以让我们代码有更好的可读性。

#### Flow 类型注解

在绝大多数情况下,Flow都可以像刚刚所说的一样他可以帮我们推断出来变量或者是参数的具体类型。

所以说从这个角度上来讲,我们实际上没有必要给所有的成员都去添加类型注解,但是我们去添加类型注解他可以更明确的去限制类型,而且对我们后期去理解这里的代码呢也是更有帮助的。

所以说我们建议大家还是尽可能去使用类型注解,那类型注解呢,它不仅仅可以用在我们函数的参数上,这里还可以用来去标记变量的类型以及我们函数返回值的类型。

那用在变量上就是在我们变量名后面跟上:类型的名称,那这样的话我们这个变量就只能够去存放这种类型的数据,如果说我们赋值的是其他类型的数据,就会报出语法错误。

ts
let num: number = 123;

去标记函数返回值类型呢就是在函数的参数括号后面去跟上:类型名称,那此时这个函数就只允许去返回这个类型的返回值,如果返回的是一个其他类型的值,这里也会报出语法错误。

ts
function foo(): number {
return 100;
}

这里还有一个需要注意的地方就是, 如果说函数没有返回值的话,在js当中没有返回值默认返回的就是undefined,所以说他也会报语法错误。对于我没有返回值的函数我们应该将它的返回值类型标记为void。

ts
function foo(): void {

}

#### Flow 原始类型

在用法上Flow几乎没有任何的难度,那无外乎就是使用Flow命令去根据我们代码当中添加的类型注解去检测我们代码当中的那些类型使用上的异常。

那这里值得我们再去了解的无外乎就是Flow当中具体支持哪些类型以及我们在类型注解上有没有一些更高级的用法,这里我们具体来看。

在Flow当中能够使用的类型有很多,首先最简单的自然就是js当中所有的原始数据类型,我们可以快速来尝试一下。

在js当中的原始数据类型现在应该是有6种,分别是string,number,boolean,null,undefined 以及最新ES6提供的 symbol。当然在以后还会有一个bigInt,现在刚刚被标准化。

这里我们可以快速尝试一下,首先是string类型,那这种类型的变量呢,他要求只能够去存放字符串类型,这没什么可说的。

ts
const a: string = 'foobar';

然后就是number类型,number类型的变量他可以用于存放数字,它还可以用来存放NaN, 这个表示的是一个非数字,在js当中他也属于number类型, 所以number类型变量也可以用于存放NaN。

除此之外还有一个Infinity,也就是无穷大,这也是js当中number的一个特殊值,表示无穷大的一个值。

ts
const b: number = NaN;

再有就是boolean类型,那这种类型值能够存放两个值,一个是true,一个是false,这也没什么特殊情况。

ts
const c: number = NaN;

那再然后就是null,那这个类型只有一种情况,就是他本身。

ts
const d: null = null;

与null相类似的还有一个就是undefined, 这里需要注意的是Flow当中这个undefined是用volid表示的。也就是说我们要想给一个变量当中去存放undefined,我们需要把它的类型标记为void,这一点跟函数返回值返回undefined是一个道理

ts
const e: void = undefined;

最后还有我们在ES6中提供的symbol,那他就只能存放symbol类型的值了。

ts
const f: symbol = Symbol();

其实我们也不需要刻意记忆这些Flow当中的原始类型,你多去用一用自然也就熟悉了。

#### Flow 数组类型

除了对普通的数值做类型限制的原始类型,在Flow当中还支持对有结构的数据做类型限制,那这里有结构的数据无外乎就是对象或者是数组,那这里我们先来看一下数组类型。

那Flow当中支持两种数组类型的表示方法,第一种就是使用Array类型,不过这个类型他需要一个泛型参数,用来去表述这个数组当中的每一个这个数组当中的每一个元素的类型。

我们可以在Array的后面用一对尖括号去指定,例如我们这里把元素的类型指定为number。

ts
const arr: Array = [1, 2, 3, 4];

关于泛型这个概念呢, 我们在后面的ts当中会去详细介绍,这里我们就只需要知道,这个```Array<number>```表示的是一个全部由数字组成的数组,那这个变量的值也就必须要是一个全是数字的数组。

如果我们在数组当中出现了其他的类型,就会报出语法错误。那这是第一种方式。

第二种方式就在元素类型后面跟上一个数组类型的方括号,这种方式同样可以表示一个全部由于数字组成的数组。

ts
const arr: number[] = [1, 2, 3, 4];

以上就是表示数组类型的两种方法,那除此之外如果我们需要去表示一个固定长度的数组,我们可以使用一种类似于数组字面量的语法去表示。

例如我们这里定义一个叫做foo的变量,那他的类型是一个数组,然后在数组当中我们第一个类型放的是string类型,第二个元素是number类型,那这个时候我们在这个变量当中就只能够去存放一个包含两位元素的数组。

ts

const foo: [string, number] = ['foo', 100];

而且第一个元素必须是字符串,第二个是数字。

那对于这种固定长度的数组,我们有一个更专业的名称,叫做元组,一般这种元组我们在一个函数当中同时要去返回多个返回值的时候我们就可以使用这种元组的数据类型。

#### Flow 对象类型

对象是js当中最常见的数据结构,在Flow当中去描述对象类型的方式跟对象字面量语法非常类似,那我们具体来看。

这里我们先去定义一个obj的变量,如果我们需要限制这个变量只能够是对象类型的话,那我们就可以在他的类型注解上使用一对花括号。

然后在这对花括号里面我们就可以去添加具体的成员名称和对应的类型限制,例如我们这里先去添加一个叫做foo的成员, 然后他的类型是string,然后我们可以再去添加一个bar 的成员,那这个类型我们给他设置为number。

ts

const obj: {foo: string, bar: number } = {foo: 'str', bar: 100 };

那这样的话就表示我们在当前这个变量当中所存放的对象他必须要具有foo和bar这两个成员,而且他们的类型分别是string和number。

那如果我们需要其中某一个成员是可选的,那我们就可以在这个成员的名称后面添加一个?, 那这样的话这个成员就是可有可无的。

ts
const obj: {foo?: string, bar: number } = { bar: 100 };

那除此之外对于对象很多时候我们会把他当做键值对集合去使用,也就是我们在初始化对象时并不去添加任何的属性,然后在后续代码执行的过程中动态去添加一些键值。

那这种情况默认就是被允许的,不过在默认这种情况下我们还是可以使用任意类型的键和任意类型的值。

那如果我们需要明确去限制键和值的类型,那我们这里可以使用一种类似索引器的语法去设置,也就是把对象类型当中的这个属性名位置修改为[], 然后在里面去指定他的键是什么类型的,那我们这里设置为string然后冒号后面仍然使我们值对应的类型。

ts
const obj: { [string]: string} = {};

那这种类型键值的意思呢就是表示我们当前这个对象允许添加任意个数的键,不过呢他的键的类型和值的类型都必须是字符串。

那这就是对象的一些类型的限制。

#### Flow 函数类型

对于函数的类型限制,一般指的就是对函数的参数类型和返回值类型进行对应的约束。

那对于函数的参数类型限制我们可以在参数的名字后面跟上一个类型注解。然后对于返回值的类型是在函数的这个括号后面去添加对应的类型注解,那这点之前我们已经介绍过了。

除此之外呢,因为函数在js当中也是一种特殊的数据类型,很多时候我们也会把函数放到变量当中,例如我们去传递回调函数作为参数的时候,就会把一个函数放到一个回调参数的变量当中。

那这种情况下我们该如何去限制我们存放函数的这种变量的类型呢。我们具体来看。

那这里呢我们可以先去定义一个函数,那这个函数他接收一个回调函数参数,然后在这个函数的内部我们去调用这个callback参数。

js
function foo (callback) {
callback('string', 100);
}

如果我们想去限制这个回调函数的参数和他的返回值,那这里我们可以使用一种类似于箭头函数的一种函数签名的类型去限制,例如我们这要去要求这个回调函数他必须要有两个参数,第一个是string类型的第二个是number类型的。

然后我们在箭头的右边,也就是返回值的位置我们去指定这个函数的返回值是void,也就是说他没有返回值。

ts
function foo (callback: (string, number) => void) {
callback('string', 100);
}

那此时我们再去调用这个foo函数的时候我们传入的这个回调函数那就必须要遵循刚刚这样一个限制,也就是他可以接收两个参数,分别是string和number,然后在这个函数内部是不可以有返回值的,或者说是返回undefined。

ts
function foo (callback: (string, number) => void) {
callback('string', 100);
}

foo(function(str, a) {
// str => string;
// a => number
})

这就是函数的类型限制。

#### Flow 特殊类型

除了常规的几种原始数据类型以外,在Flow当中还支持几种特殊的类型或者说是几种特殊的情况。这里我们来依次了解一下。

首先是一个叫做字面量的类型,与传统的类型不同的是,这种字面量类型他是用来去限制我们的变量必须是某一个值。

例如我们这声明一个叫做a的变量,然后他的类型我们用一foo的字符串去表示,那这是一个字面量,那此时我们这个a变量当中就只能够去存放这个字符串的值,也就是一个foo字符串。如果是其他任何的字符串都会报错。当然其他类型更不用说了。

ts
const a: 'foo' = 'foo';

那这种字面量类型他一般不会单独去使用,而是配合一个叫联合类型的用法去组合几个特定的值,例如我们这定义一个叫做type的变量,然后他的类型是success|warning|danger。

const type: 'success' | 'warning' | 'danger' = 'success';

那此时我们这个type变量就只能存放这三种值的其中之一, 那这就是字面量类型。

那再者就是刚刚我们所看到的这种联合类型的用法,或者也可以叫做或类型,那他不仅可以用在我们这种字面量上面,还可以用在普通的类型上面。

例如我们这里定义一个b变量,那这个变量的类型是string | number, 那也就是说我们这个变量的值可以是字符串或者是数字都是没有问题的。

ts
const b: string | number = 'string'; // 100

我们这还可以使用type关键词去做一个单独的声明去声明一个类型,用来去表示多个类型联合过后的结果。

例如我们这定义一个叫做StringOrNumber的一个类型,那他这个类型的具体情况呢就是string或者number,那这样的话我们这个StringOrNumber他就是一个相当于一个类型的别名。

ts
const StringOrNumber = string | number;
const b: StringOrNumbe = 'string'; // 100

然后我们可以在多个地方重复去使用这个类型了。

那这就是通过type关键词去给类型做一个别名或者可以说是用type去单独声明一个类型。

那除此之外在Flow当中还支持一种叫做maybe的类型,就是有可能,例如我们这定义一个叫做gender的变量,那他的类型是number,那此时这个变量他是不能为空的,也就是不能是null或者undefined。

那如果我们需要这个变量可以为空的话,我们就可以在这个number前面去添加一个问号,去表示我们这个变量除了可以接收number以外它还可以接收null或者undefined。

const gender: ?number = null;

那也就是这种maybe类型他在具体的类型基础之上扩展了null和undefined这两个值,那这种用法呢实际上就相当于是number | null | void, 那这就是我们所谓的maybe类型。

#### Flow Mixed & Any

那除此之外呢还有两个特殊的类型呢就是mixed和any,那我们分别来看一下。

那首先是mixed类型,那这个类型呢他可以用来去接收任意类型的值,我们这里可以尝试定义一个叫做passMixed的函数,然后我们在这个函数中去接收一个value参数,这个参数类型我们标记为mixed。

ts
function passMixed(value: mixed) {

}

那此时我们去调用这个函数时我们就可以传入任何类型的数据了。那这种叫做mixed的类型它实际上就是所有类型的一个联合类型。

那除了mixed类型可以接收所有类型以外,还有一个叫做any的类型,他也有类似这样的一个效果。

那这里我们再来定义一个叫做passAny的函数,那这个函数同样接收一个value参数,然后我们将这个value标记为any类型。

ts
function passAny(value: any) {

}

那此时我们依然可以对这个函数传入任意类型的参数,结果也都不会有任何的问题。

那此时你可能就会在想,既然都是接收任意类型的数据,那他们两者之间的差异到底在什么地方呢。

那可以用一句话来说就是any他是弱类型,而我们的mixed他仍然还是强类型的,那为什么这么说呢,我们这试验一下就可以明白。

这里我们在passAny函数的内部我们可以把这个value当做任意类型去使用,例如我们当成字符串,那我们就可以调用他的substr方法,当然我们只是举其中一个例子,或者你也可以把他当做一个数字。结果都不会报错,当然了这里说的不会报错指的是语法上不会报错。并不是说运行的阶段不会报错。

ts
function passAny(value: any) {
value.substr(1)
}

那也就是说我们这个any他仍然是弱类型的。

而mixed他则完全不一样,那如果说我们也是相同的直接把他当做字符串或者是数字去使用,结果就会直接报处语法错误。

因为在这个地方mixed类型他是一个具体的类型,如果我们没有明确他内部是一个字符串的话,那我们是不可以把他当做字符串去使用的。

那这里我们想要明确这个mixed类型的value到底是不是个字符串,那我们可以通过typeof这种方式去明确,也就是我们以前传统的类型判断的方式。

我们可以在这个运行阶段去判断一下,我们这个value他到底是不是个字符串,如果是的话我们再按照字符串的方式去调用里面的substr方法或是其他别的方法,结果就不会报错了。

ts
function passMixed(value: mixed) {
if (typeof value === 'string') {
value.substr(1)
}
}

那这就很明显了,我们这里使用mixed类型,他仍然是安全的,因为对于我们这个地方如果说,在类型使用上存在隐患的话他仍然会报错,而我们加了类型判断过后,他实际上就解决了这种类型隐患。

那相比较来讲的话,any则是不安全的,所以说我们在实际使用的过程当中,尽量不要去使用any类型。

那any类型存在的意义呢,其实主要是为了兼容以前的一些老代码,因为在很多的陈旧代码当中我们可能会借助于js的弱类型或者动态类型去做一些特殊的情况。那些情况如果要被我们这个地方兼容我们就需要用到any这样一个类型。

#### Flow 类型小结

那关于Flow当中的类型我们这里只是了解了一部分常见的,其实他还有很多很多,这里呢我们不可能去一一介绍,一一介绍也没有什么太大的意义。

对于Flow的学习我们其实最主要的目的就是为了可以在以后我们去理解一些例如像vue或者是react的一些第三方项目的源码时,可能会遇到这个项目中使用了Flow的情况,所以说我们必须要能够看懂。

不过呢,在这些项目当中他可能会存在一部分我们没有了解过的类型,那这些类型呢到时候可以再去查一下相应的文档。

下面链接就是Flow的官网当中对所有类型的一个描述文档。

https://flow.org/en/docs/types

除此之外呢,我们这里推荐一个第三方的类型手册,这个类型手册整理的更为直观,更适合我们在当前这种了解过Flow过后,然后再去做对应的查询。

https://www.saltycrane.com/cheat-sheets/flow-type/latest/

如果说我们在访问这些链接的时候遇到打开的情况,就可以借助于科学上网的工具去访问。

#### Flow 运行环境

最后呢我们还需要在了解一下Flow当中对一些运行环境所提供的一些API的支持,那为什么这么说呢,因为我们的js他不是独立工作的,他必须要运行在某一个特定的运行环境当中。

例如浏览器环境或者是Node环境,那这个运行环境呢他一定会提供一些API给我们使用。

例如我们在浏览器环境中有DOM和BOM,在node环境中有各种各样的模块,那这也就是说我们的代码他必然会使用到这些环境中所提供的一些API或者是一些对象。

那对于这些API和对象,他同样会有一定的类型限制,例如我们调用一下DOM中获取元素的方法。这个方法就要求我们必须要传入一个字符串,如果我们传入数字的话就会报错。

ts
document.getElementById('app');

那这就属于浏览器环境所内置的一些API所对应的一些类型限制,那这个函数的方法的返回的就是一个HTMLElement类型,而且如果我们没有找到对应的元素他还有可能返回null。

ts
const element: HTMLElement | null = document.getElementById('app');

那这就属于运行环境所内置的一些类型限制。下面列出Flow针对不同环境提供的一些API。

https://github.com/facebook/flow/blob/master/lib/core.js

https://github.com/facebook/flow/blob/master/lib/dom.js

https://github.com/facebook/flow/blob/master/lib/bom.js

https://github.com/facebook/flow/blob/master/lib/cssom.js

https://github.com/facebook/flow/blob/master/lib/node.js

#### TypeScript 概述

TypeScript是一门基于JavaScript基础之上的编程语言,很多时候我们都在说,他是一个JavaScript的超集或者叫扩展集。

所谓超级其实就是在JavaScript原有基础之上多了一些扩展特性,那多出来的实际上就是一套更强大的类型系统,以及对ECMAScript的新特性的支持。

那他最终会被编译为原始的JavaScript,那这也就是说我们去使用TypeScript过后,我们开发者在开发过程当中就可以直接去使用TypeScript所提供的这些新特性,以及我们在TypeScript当中他有更强大的类型系统,然后去完成我们的开发工作。

那在完成开发工作过后呢,我们再将我们的代码编译成能够在生产环境直接去运行的JavaScript代码,那他的作用也就非常明显了。

那TypeScript当中的类型系统的优势呢其实我们在之前已经有所体会了,因为他跟Flow当中是类似的。无外乎就是帮我们避免我们在开发过程中有可能会出现的类型异常,然后去提高我们编码的效率,以及我们代码的可靠程度。

那对于新特性的支持呢也不用多说,因为ECMAScript在近几年迭代了很多非常有用的新功能,但是很多时候在一些陈旧的环境中都会有一些兼容性的问题,那TypeScript他就支持自动取转换这些新特性,所以说我们就可以立即去使用这些新特性,在TypeScript当中。

那也就是说,即便说我们不需要类型系统,就是我们不想要这种强类型系统,那通过TypeScript去使用ECMAScript的新特性,也是一个很好的选择。

那我们之前都是使用babel去转换我们JavaScript当中的一些新特性,那其实TypeScript在这方面跟babel实际上是类似的,因为TypeScript最终他可以选择最低应该是能编译到ECMAScript3版本的代码,所以说他的兼容性特别的好。

另外,因为TypeScript最终编译成JavaScript去工作,所以说任何一个JavaScript运行环境下的运行程序都可以使用TypeScript去开发,例如我们传统的浏览器应用或者是node应用,或者是rn,也或者是桌面应用。他们都可以使用TypeScript去开发。

那相比较于之前所介绍的Flow,那TypeScript呢他作为一门完整的编程语言,那他的功能要更为强大,而且还有一个很重要的点,他的生态更加健全更加完善,特别是对于开发工具这一块,那微软自家的开发工具对TypeScript的支持都特别友好。

像我们之前在vscode当中去使用Flow的话你会感觉有些迟钝有些卡,但是呢你去使用TypeScript的话,你会感觉非常的流畅,那目前有很多大型的开源项目都已经开始使用TypeScript去开发了,那最知名的当然也就是谷歌额angular和vue3.0。

慢慢的你会发现TypeScript已经可以说是前端领域当中的第二语言,那如果说你是小项目,需要灵活自由,那自然会选择JavaScript本身,相反如果说你是长周期开发的大型项目,那所有的人都会建议你选择使用TypeScript。

当然了,再美好的东西他一般都会有些缺点,那TypeScript他最大的缺点就是这个语言本身多了很多概念,就是相比较JavaScript来说,例如像接口,泛型,枚举等等一系列这样的概念。

那这些概念就会提高我们去学习他的一个学习成本,不过还在TypeScript它属于渐进式的,就是即便是我们什么特性都不知道,我们也可以立马按照JavaScript的标准语法去编写TypeScript代码,说白了也就是可以完全把他当中JavaScript去使,然后我们在学习的过程中了解到了一个特性我们就可以使用一个特性。

那再者就是对于周期比较短的小型项目,TypeScript他有可能会去增加一些开发成本,因为我们在项目的初期我们回去编写很多的类型声明,比如说我们的这些对象,我们的这些函数,他都会有很多的类型声明,需要我们去单独的编写。

那如果说是一个长期维护的大型项目的话那这些成本根本不算什么,而且很多时候都是一劳永逸的,那这样的话,他给我们整体带来的优势呢,实际上是远大于这点小问题的。

所以整体上来讲的话,TypeScript是我们前端这个行业再向后发展应该是一门必要的语言了。

#### TypeScript 快速上手

下面我们快速了解一下TypeScript的基本使用,那想要使用TypeScript的话,首先需要安装他,TypeScript本身就是一个npm的模块,可以选择安装到全局,但是考虑到项目依赖的问题我们这里安装到项目中,这样的话更加合理。

npm install typescript --save-dev

安装完成过后,这个模块在node_modules/.bin中就会多出一个tsc的命令,这个命令的作用就是用来帮我们去编译TypeScript代码。

那有了这个命令之后我们就可以去新建一个TypeScript文件,先去做一个最基本的尝试,这里我们新建一个started.ts文件,注意这里,TypeScript的扩展名呢默认就是ts。

在这样一个文件当中我们就可以使用TypeScript去编写代码了,不过在这里我们还没有去了解任何的TypeScript语法,那没有关系,我们之前说过了,因为TypeScript他是基于JavaScript基础之上的,所以我们完全可以按照JavaScript的标准语法在这里呢去编写代码。

而且由于TypeScript他支持最新的ECMAScript标准,所以说我们可以按照最新的标准去编码,那这里我们尝试使用const去定义一个hello,他的值是一个箭头函数,这个函数我们去接收一个name参数,然后在这个函数的函数体中我们是打印一下这个name参数。

完成以后要我们调用一下这个hello函数,传入一个字符串。

ts
const hello = name => {
console.log(Hello, ${name});
}

hello('TypeScript');

那这样一串代码就是按照最新的ECMAScript的标准呢去编写的一段普通的JavaScript,并没有任何特殊的用法,我们这里直接去尝试使用TypeScript编译一下这个文件。

这里我们可以通过npx去找到tsc命令,传入入口文件路径。

npx tsc started.ts

完成过后呢在跟目录下就会多出一个同名的js文件,那我们可以打开这样一个js文件,那这时候你就会发现,我们这里所使用的所有的ES6部分都会被转换成标准的ECMAScript3的标准的一个代码。

那这也就印证了之前我们所说的,即便是我们不需要TypeScript所提供的类型系统,我们也可以使用TypeScript直接使用最新的ECMAScript标准,那这也可以说是TypeScript当中一个主要的功能。

那除了编译转换ES的一些新特性,那TypeScript更重要的呢就是,为我们提供了一套更强大的类型系统。

那我们可以再回到这个ts文件当中,那这里我们去使用这个类型系统的方式跟我们之前在Flow当中的一些方式基本上是完全相同的。

例如我们这里需要限制这个name参数是string类型,那我们同样可以在他的后面添加一个:string,这样的一个类型注解。

那此时如果我们在外界调用时传入的不是一个字符串,而是一个别的数据类型,那这个时候我们再次去编译,就会报出语法错误。

ts
const hello = (name: string) => {
console.log(Hello, ${name});
}

// hello('TypeScript');
hello(100);

而且呢,我们的vscode默认就支持对TypeScript的语法做对应的类型检查,所以说我们这不用等到编译,在编辑器当中我们就可以直接看到所有的错误提示。

那这里我们尝试编译一下这个文件。

npx tsc started.ts

那此时我们编译的这个过程也会报出对应的错误,当我们将100改成字符串的时候就会编译完成通过。

这时候我们打开输出的js文件可以发现,我们之前添加的一些类型注解,同样被移除掉了。

最后我们再来总结一下我们这里使用TypeScript的一个基本过程。

那首先呢我们应该先安装一个TypeScript模块,那这个模块呢他提供了一个叫做tsc的命令,这个tsc实际上就是typescript-compare,我们通过这样一个命令的作用呢就是去编译TypeScript文件。

那在编译过程中呢,TypeScript首先会先去检查我们代码中的类型使用异常,然后会去移除掉我们一些类型注解之类的一些扩展的语法,并且在这个过程中他还会自动转换ECMAScript的新特性。

那这就是TypeScript的一个基本使用。

#### TypeScript 配置文件

TypeScript支持通过一个叫做tsconfig.json的文件作为配置文件,我们可以通过tsc --init生成TypeScript的配置文件,一般来说,tsconfig.json文件所处的路径就是当前项目的根路径。

tsc --init

可以发现,在根目录中多了一个tsconfig.json文件,我们打开这个文件来看一下。

json
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */

/* Basic Options */
// "incremental": true,                   /* Enable incremental compilation */
"target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [],                             /* Specify library files to be included in the compilation. */
// "allowJs": true,                       /* Allow javascript files to be compiled. */
// "checkJs": true,                       /* Report errors in .js files. */
// "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true,                   /* Generates corresponding '.d.ts' file. */
// "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true,                     /* Generates corresponding '.map' file. */
// "outFile": "./",                       /* Concatenate and emit output to single file. */
// "outDir": "./",                        /* Redirect output structure to the directory. */
// "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true,                     /* Enable project compilation */
// "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
// "removeComments": true,                /* Do not emit comments to output. */
// "noEmit": true,                        /* Do not emit outputs. */
// "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

/* 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": true,                  /* Parse in strict mode and emit "use strict" for each source file. */

/* Additional Checks */
// "noUnusedLocals": true,                /* Report errors on unused locals. */
// "noUnusedParameters": true,            /* Report errors on unused parameters. */
// "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */

/* Module Resolution Options */
// "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
// "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [],                       /* List of folders to include type definitions from. */
// "types": [],                           /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */

/* Source Map Options */
// "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

/* Experimental Options */
// "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */

/* Advanced Options */
"skipLibCheck": true,                     /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true  /* Disallow inconsistently-cased references to the same file. */

}
}

compilerOptions用来配置编译选项,可以发现里面很多内容被注释掉了,而且在每个选项上都会配有一些简要的说明。

那这里呢我们可以先来看几个最常用的到的选项。

首先是这里的一个target属性,那这个选项他的作用就是用来去设置编译后的JavaScript所采用的ECMAScript标准,那这里目前配置的是ES5, 也就是说在我们的代码当中会把所有的一些新特性都会转换成ES5标准的代码。

那我们可以尝试把他修改为ES2015, 那这样的话在我们的输出结果当中就不会再去转换ES6的特性了。

"target": "es2015",

然后在下面是一个module的选项,那这个选项指的就是我们输出的代码他会采用什么样的方式去进行模块化,当前这样一个配置选项是commonjs,也就是他会把导入导出的操作最终都编译成commonjs当中的那个require和module.export的方式。

再往下我们经常还会用到outDir,那这个用来设置编译结果输出到的文件夹,我们可以设置为dist目录。

json
"outDir": "dist"

然后还有一个叫做rootDir, 那他的作用就是配置我们源代码,也就是TypeScript代码所在的文件夹,那一般我们都会把源代码放在src目录。

json
"rootDir": "src"

然后还有一个叫做sourceMap的选项,那这个选项我们可以尝试把他设置为true,也就是开启源代码映射,那这样的话我们在调试的时候就能够使用这个source-map文件去调试TypeScript的源代码了,非常的方便。

那再往下就是类型检查的一些相关配置了,那这里默认开启了一个strict,也就是开启了严格模式,那这个选项的作用就是开启所有严格检查选项,那在这种情况下,对于类型的检查会变得十分严格。

我们这里可以举一个例子,那我们删掉name参数对应的注解,那这里的name参数就会被隐式推断为Any类型,那这种用法在严格模式下是不被允许的。

严格模式下他需要我们为每一个成员去指定一个明确的类型,即便他是Any,那也需要明确去指定,不能隐式推断。

ts
// const hello = (name: string) => {
const hello = (name) => {
console.log(Hello, ${name});
}

hello('TypeScript');

当然这里还有很多其他的配置选项,那这些其他的配置选项我们到用到的时候再去详细介绍。

那有了这个配置文件过后呢我们再去使用tsc这个命令的时候我们就可以去使用这个配置文件了。

但是这里我们需要注意的是,如果我们还是使用tsc命令去编译某一个指定的文件,那这里的配置文件是不会生效的。

npx tsc started.ts

那只有我们直接运行tsc这样一个命令编译整个项目的时候, 我们这个配置文件呢他才会自动生效。

npx tsc

但是我们运行这个命令的时候,结果确报出了一个错误,说的是我们这里的文件并没有放置在我们配置的src这样一个目录当中,那这也就证明了我们的配置文件已经开始工作了。这里我们修改一下,将started.ts移动到src目录中。对于之前生成的js文件我们这里删除掉。

那我们再次运行tsc命令就会按照我们的配置将src目录下所有的TypeScript文件编译到dist当中了。

那我们可以打开输出的dist目录,那这里不仅生成了编译过后的js文件,还有对应的source-map文件,而且我们这里所生成的js文件他是按照ES6的标准去输出的。

那这就是我们对配置文件的一个简单的了解,以及我们使用了配置文件过后我们应该怎样去在运行这个命令的时候让他工作。

#### TypeScript 原始类型

接下来我们先来了解一下,目前我们JavaScript当中6种原始数据类型,在TypeScript当中的一个基本应用。

那这里绝对多数的情况跟我们在Flow当中了解到的都是类似的,我们先将配置文件当中的target修改为常用的es5,因为这样的话我们才能发现一部分的特殊情况。这里我们快速过一下这几个原始数据类型。

那首先是string类型,那他是只能够存放字符串的。

ts
const a: string = 'footer';

然后是number类型,那他的值就只能够存放数字, 当然这个number也包括NaN和Infinity这两个特殊值。

ts
const b: number = 100;

那再然后就是boolean类型,那这个类型呢他就只能够存放true或者是false

ts
const c: boolean = true; // false

那这三种原始值类型相比较Flow当中有所不同的是,TypeScript当中这三种类型默认是允许为空的,也就是这里我们可以为他们赋值为null,或者是undefined。例如我们这里定一个字符串类型的变量吗,我们让他等于null。

ts
const d: string = null;

但是我们通过试验发现这里的结果却是错误的,那这就要说到这个严格模式和默认模式之间的一个差异了,我们将tsconfig.json中的strict设置为false,也就是关闭严格模式。这个时候我们回到代码当中就不会报错了。

那这是在非严格模式下,我们的string,number和boolean他们都可以为空,那严格模式呢他是不允许的。

当然我们这里的strict他是开启了所有的严格检查的选项,那如果说我们只是要去检查我们的变量不能为空的话我们可以使用strictNullChecks这样一个单独的选项,那这样一个选项他就是完全用来检查我们变量不能为空的。

那再者呢就是一个void类型,也就是空值,那这种类型呢一般我们会在函数没有返回值的时候我们去标记这个函数没有返回值类型,那他只能存放null或者是undefined,那在严格模式下他只能是undefined。

ts
const e: void = undefined;

然后就是null类型和undefined类型,那这两个类型本身并没有什么特殊的情况,这里我们不用管。

ts
const f: null = null;

const g: undefined = undefined;

那最后就有一个需要注意的东西了,那这个地方我们再来尝试一下ECMAScript2015当中新增的symbol类型,那这个类型他同样只能够用于存放symbol类型的值,当时呢,让我们直接去使用这样一个类型去创建symbol的值过后呢我们会发现这里的代码报出了一个错误。

ts
const h: symbol = Symbol();

那为什么这个地方会报出错误呢,我们下面介绍。

#### TypeScript 标准库声明

刚刚我们在尝试使用全局的symbol函数去创建一个symbol的值的时候报出了错误,那想要解决这个问题很简单,我们只需要按照错误描述中的一个提示,直接将编译选项的target修改为es2015就好了。

但是这到底是为什么呢,那这里我们仍旧将target设置为es5,我们继续去探索一下这样的问题,其实这个原因很简单。

我们应该知道symbol他实际上就是一个JavaScript当中内置的一个标准对象,就跟我们之前所使用的的Array,Object这些性质是相同的,只不过symbol他是ES6当中新增的,对于这种内置的对象,其实他自身也是有类型的。

而且这些内置对象的类型都在TypeScript当中已经帮我们定义好了,那这里我们可以先去使用一下Array,然后我们在这个Array上面通过右键找到Go to Definition, 也就是转到定义。

那这样的话我们就可以找到这个类型的声明文件,在这个文件中声明了所有内置对象对应的类型。

那按照道理来说symbol他也应该在这个文件当中有对应的类型声明,但其实细心一点你就已经发现这里的这个声明文件它叫做lib.es5.d.ts。那很明显我们这个文件它实际上是ES5标准库所对应的一个声明文件。

而我们的symbol他是ECMAScript2015当中所定义的,所以自然不会在这个文件当中去定义他对应的一些类型。

那我们可以再回到TypeScript的配置文件当中,那这里我们设置的target是es5,那对于ES5这种情况下,那我们对标准库的引用默认只会引用ES5所对应的标准库,所以我们的代码当中直接去使用ES6的symbol就会报错。

其实呢这里不仅仅是symbol,任何一个在ES6中新增的对象我们直接去使用都会遇到这样一个问题。

要解决这个问题的办法有两种,第一种是直接修改我们target为es2015,这样的话我们默认的准标准库就会引用ES6所对应的标准库。

如果说我们必须要编译到es5的话,那我们也可以使用lib选项去指定我们所引用的标准库,我们解除lib选项的注释,然后我们在这个数组当中添加一个ES2015, 那这样也是可以的。

json
"lib": ["ES2015"]

不过这个时候我们在代码中添加一句console.log会发现,console会报错,那出现这个问题的原因实际上跟我们刚刚所分析的是一样的。

console对象在浏览器环境中是BOM对象提供的,而我们刚刚所设置的lib当中只设置了一个ES2015, 所以说所有的默认的标准库都被覆盖掉了,那这里我们需要把这些默认的标准库再把它添加回来。

不过我们需要注意一点的是TypeScript当中把BOM和DOM都归结到一个标准库文件中了,就叫做DOM,也就是我们这里只需要添加一个DOM的引用就可以了。完成以后console就不再报错了。

那我们也可以在这个console上右键转到Go to Definition,那这样我们就可以看到刚刚所添加进来的这个DOM的标准库文件。

那以上我们通过解决代码当中找不到symbol这样一个问题,介绍了一下在TypeScript中标准库的一个概念。

那所谓标准库,实际上就是内置对象所对应的声明文件,那我们在代码当中使用内置对象就必须要引用对应的标准库,否则TypeScript就找不到所对应的类型,那他就会报错。

#### TypeScript 中文错误消息

这里我们再来介绍一个对使用中文的开发者非常有帮助的一个小技巧,就是让TypeScript去显示中文的错误消息。

那TypeScript它本身是支持多语言化的一个错误消息的,默认他会根据操作系统对开发工具的语言设置选择一个错误消息的语言。

但是我们这里所使用的是英文版的vscode,所以我们看到的错误消息都是英文的。

那如果我们想强制他显示中文消息,那我们可以在使用tsc命令的时候加上一个--local的这样一个参数,然后我们后面跟上一个zh-CN, 也就是说我们使用中文的错误消息。

tsc --local zh-CN

那这样的话对于绝大多数的错误都会以中文的形式去显示错误消息,那对于vscode当中的错误消息我们可以在配置选中当中去配置。

那我们打开配置选项,然后我们搜索一下typescript local 我们找到这样一个选项,把他设置为zh-CN。

那此时呢,我们的vscode当中所报出来的错误提示也就是中文的了。

不过这里我们并不推荐大家这么做,因为在很多时候我们对于一些不理解的错误我们会使用google这样的搜索引擎去所有一些相关的资料,那如果说我们看到的是中文的错误提示,那根据这种中文的错误消息呢我们是很难搜索到有用的东西的。

所以说一般对于开发相关的地方呢,我们建议更多的还是使用英文的方式。

#### TypeScript 作用域问题

那我们在学习TypeScript的过程当中,肯定要涉及到在不同的文件当中去尝试TypeScript的一些不同的特性,那这种情况下我们就可以能遇到不同文件当中会有相同变量名称的这样一个情况。

例如我们在两个不同的文件当中定义相同的变量名,那此时呢我们这个TypeScript就会报出一个重复定义变量的这样一个错误。

那这就是因为我们这个a变量目前他是定义在全局作用域上面的,所以TypeScript在去编译整个项目的时候就会出现这样一个错误,那解决这个问题的办法呢自然是把他们分别装到不同的或者单独的作用域当中。

例如我们可以用一个立即执行函数创建一个单独的作用域,那这个时候我们把变量放到这个函数的内部,那这样就不会有错误了。

js
(function() {
const a = 123;
})()

或者我们也可以在这个文件中使用export去导出一下,也就是使用一下ES Module, 那这样的话我们这个文件他就会作为一个模块。

js
export const a = 123;

那模块是有单独的模块作用域的, 那这里我们可以在文件的最后去添加一个export {},那需要注意这里的{}只是export的一个语法,他并不是导出了一个空对象,那这样的话我们在这个文件当中,所有的成员,他就变成了我们这个模块作用域中的局部成员了,也就不会再出现冲突的问题了。

那这样一个问题呢,实际上我们在实际开发时呢一般不会用到,因为在绝大多数情况下我们每个文件都会以模块的形式去工作,只不过我们后面的每个案例它里面难免会用到一些重复的变量,所以说我们每个案例中我们都会添加一个export。

#### TypeScript Object类型

TypeScript中的Object他并不单指普通的对象类型,而是泛指所有的非原始类型,也就是对象,数组还有函数。

例如我们这里再来定义一个叫做foo的变量,我们把它的类型标记为object,那注意这里的object是全小写的。那这样一个变量他就能接收对象,或者是数组或者是函数都是可以的。

ts
const foo: object = function() {}

那如果我们去接收其他任何一种原始值,那他会发生错误,那这里我们尤其需要注意的就是object类型他并不单指普通的对象。

那如果我们需要普通的一个对象类型, 我们需要使用类似对象字面量的语法去标记我们这里的类型, 例如我们这里去限制我们的对象必须有一个叫做foo的number类型的属性,我们就用foo: number。

ts
const obj: { foo: number } = { foo: 123 }

如果我们需要去限制多个成员,这里我们仍然可以用逗号的方式在后面继续去添加。

那这里的对象类型限制呢,他的要求是赋值的对象结构他必须要跟我们这里类型的结构完全一致,不能够多,也不能够少,也就是说如果我们在对象当中去添加了一些额外的成员,那这里语法上就会报出类型错误。

ts
const obj: { foo: number, bar: string } = { foo: 123, bar: 'string' }

那当然,在TypeScript当中去限制对象类型,我们应该使用更为专业的一个叫做接口的东西,那关于接口呢,我们会单独介绍。这里我们只需要了解两个点。

第一呢就是object类型他并不单指对象,而是除了原始类型以外的其他类型,第二点就是对象的类型限制我们可以使用这种类似对象字面量的语法的方式,但是呢,我们说了,更专业的方式是使用接口,而接口在后面我们会有详细的介绍。

#### TypeScript 数组类型

TypeScript当中定义数组的方式跟Flow当中几乎完全一致,那他也有两种方式,第一种就是使用Array泛型,例如我们这里定义一个arr,那他的类型就是Array,这里的元素类型我们可以设置为number,这就表示一个纯数字组成的数组。

ts
const arr: Array = [1];

那第二种方式要更为常见一些,就是使用元素类型然后加上[]这样一种形式。

ts
const arr: number[] = [1];

#### TypeScript 元组类型

元组类型是一种特殊的数据结构,其实元组就是一个明确元素数量以及每一个元素类型的数组,那各个元素的类型他不必要完全相同。

那在TypeScript当中我们可以使用类似数组字面量的这种语法去定义元组类型,例如我们这里先去定义一个tuple变量,那他的类型是一个[], 然后第一个元素是number,第二个元素是string,那这个时候我们这个tuple变量他就只能够存放两个对应类型的元素了。

ts
const tuple: [number, string] = [10, 'yd'];

那如果我们想要访问我们元组当中的某一个元素,我们仍然可以使用数组的方式去访问。

那这种元组呢,他一般可以用来在一个函数当中去返回多个返回值,那这种类型呢在现在实际上是越来越常见了,比如我们在react当中最新添加的useStatus这样的hook函数当中我们返回的就是一个元组类型。

再比如我们使用ES2017当中所提供的Object.entries这样一个方法获取对象当中所有键值数组,那这里我们所得到的的每一个键值他就是一个元组,因为他是一个固定长度的。

#### TypeScript 枚举类型

我们在应用开发过程中,经常会涉及到需要用某几个数值去代表某种状态,例如我们这里有一个文章对象,这个对象当中有一个文章的状态,我们可以使用数字去表示不同的状态,比如0代表草稿,1代表未发布,2代表已发布。

js
const post = {
title: 'Hello TypeScript',
status: 1,
}

那也就是说我们这个状态的取值他只可能是0,1,2这三个值,那如果我们直接在代码当中去使用0,1,2这样的字面量去表示状态的话,那时间久了我们就可能搞不清楚这里写的数字他到底对应的是哪个状态。

而且时间长了还可能混进来一些其他的值,例如我们去使用0,1,2以外的一个数字。

那这种情况下我们使用枚举类型是最合适的,因为枚举类型有两个特点,第一点就是他可以给一组数值分别取上一个更好理解的名字,第二点就是一个枚举中,只会存在几个固定的值。那并不会出现超出范围的可能性。

在很多传统的编程语言当中,枚举是一个非常常见的语言结构,不过在js当中并没有这样的数据结构。

很多时候我们都是使用一个对象去模拟实现枚举。

例如我们这里定义一个posTypeScripttatus对象,这个对象中有三个成员,分别是draft:0, unpublished: 1, published: 2

js
const posTypeScripttatus = {
draft: 0,
unpublished: 1,
published: 2
}

然后我们就可以在代码中使用对象中的一些属性去表示文章的状态,这样的话我们在后面使用的过程中就不会出现我们刚刚所说的那样的问题。

在TypeScript当中他有一个专门的枚举类型,我们可以使用一个enum关键词去声明枚举。

js
enum posTypeScripttatus = {
draft = 0,
unpublished = 1,
published = 2
}

需要注意的是这里我们使用的是=而不是对象当中的:,我们使用枚举他的使用方式跟对象是样的,同样也是打点调用。

我们这里枚举类型的值可以不用=号的方式去指定,如果我们不指定,默认的话这个值会从0开始累加,如果我们给枚举当中第一个成员去指定了值,那后面这些值都会在这个基础之上去累加。

枚举的值除了可以是数字以外还可以是字符串,也就是字符串枚举,我们可以把每个值的初始化为字符串,由于字符串是无法像数字一样自增长的,所以说是字符串枚举的话,就需要手动去给每个成员初始化一个明确的字符串的值。

那这种字符串枚举他其实并不常见,所以这里我们就不做过多的介绍了。那以上就是枚举类型的基本语法以及使用。

关于枚举,这里我们还需要了解一个内容就是,枚举类型他会入侵到运行时的代码,这么说可能不好理解,通俗一点来说就是他会影响代码编译后的结果。

我们在TypeScript当中使用的大多数类型他在经过编译转换过后最终都会被移除掉,因为他只是为了我们再编译过程中可以做类型检查,而枚举呢,他不会。

他最终会编译为一个双向的键值对对象,这里我们可以编译看一下。打开成成的js文件我们就可以看到。双向的键值对对象。

js
var posTypeScripttatus;
(funcion (posTypeScripttatus) {
posTypeScripttatus[posTypeScripttatus['draft'] = 0] = 'draft';
posTypeScripttatus[posTypeScripttatus['unpublished'] = 1] = 'unpublished';
posTypeScripttatus[posTypeScripttatus['published'] = 2] = 'published';
})(posTypeScripttatus || (posTypeScripttatus = {}))

那所谓的这个双向的键值对,其实就是可以通过键去获取值,可以通过值再去获取键,那我们这里仔细观察这段代码你会发现,无外乎也就是把我们枚举的名称最为对象的键去存储了枚举的值。然后再用值作为键再去存一下我们枚举的键。

那这样做的目的呢就是为了可以让我们动态的根据枚举值,也就是根据0,1,2这样的值去获取枚举的名称。

也就是说我们可以在代码当中通过所引起的方式去访问对应的枚举名称。

js
posTypeScripttatus[0]; // draft

那如果说我们确认我们代码当中不会使用所引起的方式去访问枚举,那我们就建议大家去使用常量枚举。

那常量枚举的用法呢就是在枚举的enum这个关键词前面去加上一个const。

ts
const enum posTypeScripttatus = {
draft,
unpublished,
published
}

编译过后我们再次找到生成的js,这离我们就可以发现,我们所使用的枚举会被移除掉,而我们使用枚举值的地方都会被替换为实际的值,枚举的名称会以注释的方式去标注。

以上就是在TypeScript当中枚举类型的一些主要特征。

#### TypeScript 函数类型

接下来我们来了解一下TypeScript当中函数的类型约束,那函数的类型约束无外乎就是对函数的输入输出进行类型限制。

那输入就是我们的参数输出指的就是返回值,不过呢,在JavaScript当中有两种函数定义的方式,分别是函数声明和函数表达式。

所以我们这需要分别去了解一下这两种方式下,函数如何进行具体的类型约束。

这里我们来声明一个函数,也就是使用函数声明的方式去定义一个函数,这个函数接收两个参数,在里面我们去返回一个字符串。

js
function func1 (a, b) {
return 'func1';
}

那函数声明的这种方式他的类型约束就比较简单,直接在函数每一个参数后面添加对应的类型注解, 然后函数返回值的类型注解呢需要添加在括号后面。

那添加完对应的类型注解过后呢,我们再调用这个函数时只能够传入相同类型的参数,而且这里的参数的个数也必须要完全相同。

那如果我们需要某一个参数是可选的,我们就可以使用可选参数这样一个特性, 也就是在参数的名称后面添加一个?, 那这样的话这个参数就变成了一个可选参数, 我们在调用时可以传也可以不传。

ts
function func1 (a: number, b: number, c?: number): string {
return 'func1';
}

func1(100, 200);
func1(100, 200, 300);

或者我们也可以使用ES6新增的这种参数默认值的特性,因为添加了参数默认值过后,这个参数本身就是可有可无的了。

ts
function func1 (a: number, b: number, c?: number = 10): string {
return 'func1';
}

func1(100, 200);

当然对于这里我们所使用的可选参数也好,默认参数也好他都必须要出现在我们参数列表的最后。这一点的原因其实很简单,因为参数会按照位置进行传递,如果说可选参数出现在了必选参数的前面,那这个时候必选参数是没有办法拿到正常对应的值。

如果我们需要接收任意个数的参数,我们可以使用ES6的reset操作符。

ts
function func1(…reset: number[]): string {
return 'func1';
}

那以上就是函数声明的一个对应的类型限制,那我们可以再来看一下函数表达式所对应的类型限制。

那这里我们再使用函数表达式的方式去定义一个函数。那函数表达式他也可以使用相同的方式去限制函数的参数和返回值的类型。

ts
const func2 = function(a: number, b: number): string {
return 'func2';
}

不过我们这里这个函数表达式,他最终是放到一个变量中的。那接收这个函数的变量,他也应该是有类型的。

那一般TypeScript它都能根据我们这个函数表达式推断出来我们这个变量的类型,不过呢,如果我们是把一个函数作为参数传递,也就是回调函数的这种方式,那对于回调函数这种情况下,我们就必须要去约束我们这个回调函数这个参数,也就是这个形参的类型。

那这种情况下我们就可以使用这种类似箭头函数的方式去表示我们这个参数,他可以接收什么样的一个函数。

ts
const func2: (a: number, b: number) => string = function(a: number, b: number): string {
return 'func2';
}

那这种方式我们在以后去定义接口的时候会经常用到,那具体的我们到时候再说。

#### TypeScript 任意类型

由于JavaScript自身是弱类型的关系,很多内置的API它本身就支持接收任意类型的参数。

而TypeScript又是基于JavaScript基础之上的,所以说我们难免会在代码当中需要去用一个变量去接收任意类型的数据。

例如我们这里定义一个叫做stringify的函数,这个函数接收一个value参数,在内部我们使用JavaScriptON.stringify方法将value序列化成JavaScripton字符串,那这里的这个value参数他就应该支持接收任意类型的参数。

js
function stringify (value) {
return JavaScriptON.stringify(value);
}

因为本身这个JavaScriptON.stringify就可以接收任意类型的参数,那此时我们就必须要有一个类型可以去用来接收任意类型参数。

ts
function stringify (value: any) {
return JavaScriptON.stringify(value);
}

那any就是这样一个可以用来接收任意类型数据的一种类型,需要注意的是any类型仍然属于动态类型。他的特点跟普通的JavaScript变量是一样的。也就是可以用来接收任意类型的值。而且在运行过程中它还可以用来接收其他类型的值。

那正式因为他有可能存放任意类型的值,所以说TypeScript他不会对any这种类型做类型检查,那这也就意味着我们仍然可以像之前在JavaScript中一样在他上面去调用任意的成员。语法上都不会报错。

那也正式因为any类型他不会有任何的类型检查,所以说他仍然会存在类型安全的问题,我们轻易不要使用这种类型。

但是有的时候我们在去兼容一些老的代码的时候呢我们可能难免会用到这样一个any的类型。

#### TypeScript 隐式类型推断

在TypeScript当中如果我们没有明确通过类型注解去标记一个变量的类型,那TypeScript他会根据这个变量的使用情况去推断这个变量的类型。那这样一种特性叫做隐式类型推断。

例如我们这里使用let关键词定义一个叫做age的变量,这个变量我们赋值了一个数字,这样一个变量的类型就会被TypeScript推断为number类型。

ts
let age = 18;

如果说我们这个地方再去给这个变量重新赋值一个字符串,此时语法上就会出现类型错误, 因为这个时候age他已经被推断为了number类型,也就说说这种用法实际上就相当于给age变量添加了: number的类型注解。

ts
let age = 18;

age = 'string';

那如果说TypeScript他无法去推断一个变量具体的类型,那这个时候他就会将这个类型标记为any。

例如我们这定义一个叫做foo的变量,当我们去声明这个变量的时候我们并没有为他赋值,那这个时候这个foo就是any类型, 也就是一个动态类型,那也就是说我们可以在后续代码中去往这个变量当中去放入任意类型的值。语法上都不会报错, 那这就是隐式类型推断。

ts
let foo;

foo = 'string';

虽然说我们在TypeScript当中支持隐式类型推断,而且这种隐式类型推断可以帮我们简化一部分代码,但是我们仍然建议尽可能给每一个变量去添加明确的类型,因为这样的话会便于我们后期去更直观的理解我们的代码。

#### TypeScript 类型断言

在有些特殊的情况下TypeScript无法去推断出一个变量的具体类型,而我们作为开发者,我们根据代码的使用情况我们是可以明确知道这个变量到底是什么类型的。

例如我们这里有一个数组,我们假定这个数组他是从一个可以明确的接口当中获取到的,那此时我们调用这个数组对象的find方法从这个数组当中去找出第一个大于0的数字。

js
const nums = [110, 120, 119, 112];

const res = nums.find(i => i > 0);

那很明显,这里他的返回值一定是一个数字,因为我们这个数组当中一定会有一个大于0的数字。

但是对于TypeScript来讲,他并不知道,他所推断出来我们这个地方的返回值呢是一个number或者undefined,因为他会认为我们有可能找不到。

那此时我们就不能把这个返回值当做一个数字去使用,那这种情况下我们就可以去断言res是一个number类型的。

js
const nums = [110, 120, 119, 112];

const res = nums.find(i => i > 0);

const square = res * res;

那断言的意思实际上就是明确告诉TypeScript, 你相信我,我们这个地方res一定是number类型。

那类型断言的方式有两种,第一种是使用as关键词,那我们这里再去定义一个num1的变量,让他等于res as number。那此时我们这个编译器就能明确知道我们这个num1他是一个数字了。因为我们是明确告诉TypeScript我们这里是一个number的。

ts
const num1 = res as number;

那另一种语法呢就是在这个变量的前面去使用<>的方式去断言,我们这里定义一个num2,我们让他等于<number>res。那这个效果仍然是一样的。

ts
const num2 = res;

但是这种<>的方式,他有一个小问题,就是当我们在代码当中使用了jsx的时候,例如我们在写react代码的时候,难免会用到jsx,那这里的<number>他会跟jsx中的标签产生语法上的冲突,所以那种情况下就不能使用这样的方式了。

所以这里我们推荐使用第一种as方式,那以上就是类型断言的基本用法,那这种用法很多时候我们都可以用来去辅助TypeScript去更加明确我们代码当中每一个成员的类型。

那这里还需要注意一点的是,类型断言他并不是类型转换,也就是说这里他并不是把一个类型转换成了另外一个类型。

因为类型转换他是代码在运行时的一个概念,而我们这个地方类型的断言呢他只是在编译过程中的一个概念,当代码编译过后呢,这个断言他也就不会存在了,所以说他跟我们类型转换是有本质上的差异的。

#### TypeScript 接口

例如我们这定义一个叫做printPost的函数,那这个函数可以接收一个文章对象参数post,然后在函数的内部去打印文章的title, 然后再去打印他的content属性。

js
function printPost (post) {
console.log(post.title);
console.log(post.content);
}

那这个时候对于这个函数所接收的post对象他就有一定的要求,也就是我们所传入的这个对象必须要存在一个title属性和一个content属性,只不过这种要求他实际上是隐性的,他没有明确的表达出来。

那这种情况下我们就可以使用接口去表现出来这种约束,这里我们可以尝试先去定义一个接口。

定义接口的方式呢就是使用interface这样一个关键词,然后后面跟上接口的名称,这里我们可以叫做post,然后就是一对{},然后{}里面就可以添加具体的成员限制。这里我们添加一个title和content,类型都是string。

ts
interface Post {
title: string;
content: string;
}

注意这里我们可以使用逗号分割成员,但是更标准的做法是使用分号去分割,而且呢这个分号跟js中绝大多数的分号是一样的,可以省略,那关于是否应该在代码当中明确使用每一个分号,个人的编码习惯是不加,你可以根据你所在的团队或者是项目对应的编码规范来去决定要不要加分号,这个问题我们不做过多讨论。

完成过后我们这里可以给这个post参数的类型设置为我们刚刚所定义的Post接口。

ts
function printPost (post: Post) {
console.log(post.title);
console.log(post.content);
}

printPost({
title: 'hello',
content: 'typescript'
})

那此时就是显示的要求我们所传入的对象他必须要有title和content这两个成员了,那这就是接口的一个基本作用。

一句话去总结,接口就是用来约束对象的结构,那一个对象去实现一个接口,他就必须要去拥有这个接口当中所约束的所有的成员。

我们可以编译一下这个代码,编译过后我们打开对应的js文件,我们在js当中并不会发现有任何跟接口相关的代码,也就是说TypeScript中的接口他只是用来为我们有结构的数据去做类型约束的,在实际运行阶段呢,实际这种接口他并没有意义。

###### 可选成员,只读成员

对于接口中约定的成员,还有一些特殊的用法,我们依次来看一下。

首先是可选成员,如果说我们在一个对象当中,我们某一个成员他是可有可无的话,那这样的话我们对于约束这个对象的接口来说我们可以使用可选成员这样一个特性。

例如我们这里添加一个subTitle这样一个成员,他的类型同样是string,不过我们这里的文章不一定是每一个都有subTitle,这种况下我们就可以在subTitle后面添加一个问号,这就表示我们这个subTitle成员他是可有可无的了。

ts
interface Post {
title: string;
content: string;
subTitle?: string;
}

那这种用法呢其实就是相当于给这个subTitle标记他的类型是string或者是undefined。这就是可选成员。

接下来我们再来看一下只读成员这样一个特性,那这里我们再给Post接口添加一个summary这样一个成员,那一般逻辑上来讲的话文章的summary他都是从文章的内容当中自动提取出来的,所以说我们不应该允许外界去设置他。

那这种情况下我们可以使用readonly这样一个关键词,去修饰一下这里的summary。那添加了readonly过后我们这个summary他在初始化完成过后就不能够再去修改了。如果我们再去修改就会报错。这就是只读成员。

ts
interface Post {
title: string;
content: string;
subTitle?: string;
readonly summary: string;
}

最后我们再来看一个动态成员的用法,那这种用法一般是适用于一些具有动态成员对象,例如程序当中的缓存对象,那他在运行过程中就会出现一些动态的键值。

这里我们来新建一个新的接口,因为我们在定义的时候我们是无法知道会有那些具体的成员,所以说我们就不能够去指定,具体的成员名称,而是使用一个[], 这个[]中使用key: string。

这个key并不是固定的,可以是任意的名称, 只是代表了我们属性的名称,他是一个格式,然后后面这个string就是成员名的类型,也就是键的类型,后面我们可以跟上动态属性的值为string。

ts
interface Cache {
[key: string]: string;
}

完成以后我们再来创建一个cache对象,让他去实现这个接口,那这个时候我们就可以在这个cache对象上动态的去添加任意的成员了, 只不过这些成员他都必须是stringd类型的键值。

ts
const cache: Cache = {};

cache.foo = 'value1';
cache.bar = 'value2'

#### TypeScript 类的基本使用

类可以说是面向对象编程中一个最重要的概念,关于类的作用这里我们再简单描述一下。他就是用来描述一类具体事物的抽象特征,我们可以以生活角度去举例。

例如手机就属于一个类型,那这个类型的特征呢就是能够打电话,发信息。那在这个类型下面呢他还会有一些细分的子类,那这种子类他一定会满足父类的所有特征,然后再多出来一些额外的特征。例如只能手机,他除了可以打电话发短信还能够使用一些app。

那我们是不能直接去使用类的,而是去使用这个类的具体事物,例如你手中的只能手机。

那类比到程序的角度,类也是一样的,他可以用来去描述一类具体对象的一些抽象成员,那在ES6以前,JavaScript都是通过函数然后配合原型去模拟实现的类。那从ES6开始,JavaScript中有了专门的class。

而在TypeScript中,我们除了可以使用所有ECMAScript的标准当中所有类的功能,他还添加了一些额外的功能和用法,例如我们对类成员有特殊的访问修饰符,还有一些抽象类的概念。

那对于ECMAScript标准中的class,我们这里就不单独去介绍了,如果你不太熟悉,你可以去参考ECMAScript前面的文章。

这里我们来着重介绍一下,在TypeScript中额外多出来的一些新的类的一些特性,我们可以先来声明一个叫做Person的类型,然后我们在这个类型当中去声明一下constructor构造桉树,在构造函数当中我们接收一个name和age参数,那这里我们仍然可以使用类型注解的方式去标注我们这个地方每个参数的类型。

然后在这个构造函数的里面我们可以使用this去为当前这个类型的属性去赋值,不过这里直接去使用this访问当前类的属性会报错,说的是当前这个Person类型上面并不存在对应的name和age。

ts
class Person {
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}

这是因为在TypeScript中我们需要明确在类型中取声明他所拥有的一些属性,而不是直接在构造函数当中动态通过this去添加。

那在类型声明属性的方式就是直接在类当中去定义, 那这个语法呢是ECMAScript2016标准当中定义的,那我们同样可以在这里给name和gae属性添加类型。那他也可以通过等号去直接赋值一个初始值,不过一般情况下我们还是会在构造函数中动态的为属性赋值。

需要注意的是,在TypeScript中类的属性他必须要有一个初始值,可以在等号后面去赋值,或者是在构造函数当中去初始化,两者必须做其一,否则就会报错。

ts
class Person {
name: string = 'init name';
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}

那以上就是在TypeScript中类属性在定义上的一些细微差异,其实具体来说就是我们类的属性他在使用之前必须要现在类型当中去声明,那这么做的目的其实就是为了给我们的属性去做一些类型的标注。

那除此之外呢我们仍然可以按照ES6标准当中的语法,为这个类型去声明一些方法,例如我们这里添加一个叫做sayHi的方法,那在这个方法当中我们仍然可以使用函数类型注解的方式去限制参数的类型和返回值的类型。

那在这个方法的内部呢我们同样可以使用this去访问当前实例对象,也就可以访问到对应的属性。

ts
class Person {
name: string = 'init name';
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

sayHi(msg: string): void {
    console.log(`I am ${this.name}, ${msg}`);
}

}

那以上就是类在TypeScript中的一个基本使用。

#### TypeScript 类的访问修饰符

接下来我们再来看几个TypeScript中类的一些特殊用法,那首先就是类当中成员的访问修饰符,类中的每一个成员都可以使用访问修饰符去修饰他们。

例如我们这里给age属性前面去添加一个private,表示这个age属性是一个私有属性,这种私有属性只能够在类的内部去访问,这里我们创建一个Person对象, 我们打印tom的name属性和age属性。可以发现name可以访问,age就会报错,因为age已经被我们标记为了私有属性。

ts
class Person {
name: string = 'init name';
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

sayHi(msg: string): void {
    console.log(`I am ${this.name}, ${msg}`);
}

}

const tom = new Person('tom', 18);

console.log(tom.name);
console.log(tom.age);

除了private以外,我们还可以使用public修饰符去修饰成员,意思是他是一个共有成员,不过再TypeScript中,类成员的访问修饰符默认就是public,所以我们这里加不加public效果都是一样的。不过我们还是建议大家手动去加上这种public的修饰符,因为这样的话,我们的代码会更加容易理解一点。

ts
class Person {
public name: string = 'init name';
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

sayHi(msg: string): void {
    console.log(`I am ${this.name}, ${msg}`);
}

}

最后还有一个叫做protected修饰符,说的就是受保护的,我们可以添加一个gender的属性,他的访问修饰符我们就使用protected,我们同样在构造函数中初始化一下gender。

完成过后我们在实例对象上去访问gender,会发现也是访问不到的,也就是protected也不能在外部直接访问。

ts
class Person {
public name: string = 'init name';
private age: number;
protected gender: boolean;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
this.gender = true;
}

sayHi(msg: string): void {
    console.log(`I am ${this.name}, ${msg}`);
}

}

const tom = new Person('tom', 18);

console.log(tom.name);
// console.log(tom.age);
console.log(tom.gender);

那他跟private的区别到底在什么地方呢, 这里我们可以再定义一个叫做Student的类型,我们让这个类型去继承自Person,我们在构造函数中尝试访问父类的gender,是可以访问到的。那意思就是protected只允许在子类当中去访问对应的成员。

ts
class Student extends Person {
constructor(name: string, age: number) {
super(name, age); // 子类需要调用super将参数传给父类。
console.log(this.gender);
}
}

那以上就是TypeScript当中对于类额外添加的三个访问修饰符。分别是private,protected和public,那他们的作用可以用来去控制类当中的成员的可访问级别。

那这里还有一个需要注意的点,就是对于构造函数的访问修饰符,那构造函数的访问修饰符默认也是public,那如果说我们把它设置为private,那这个类型就不能够在外部被实例化了,也不能够被继承,那在这样一种情况下,我们就只能够在这个类的内部去添加一个静态方法,然后在静态方法当中去创建这个类型的实例,因为private只允许在内部访问。

例如我们这里再去添加一个叫create的静态方法,那static也是ES6标准当中定义的,然后我们就可以在这个create方法中去使用new的方式去创建这个类型的实例,因为new的方式就是调用了这个类型的构造函数。

此时我们就可以在外部去使用create静态方法创建Student类型的对象了。

ts
class Student extends Person {
private constructor(name: string, age: number) {
super(name, age); // 子类需要调用super将参数传给父类。
console.log(this.gender);
}

static create(name: string, age: number) {
    return new Student(name, age);
}

}

const jack = Student.create('jack', 18);

那如果我们把构造函数标记为protected,这样一个类型也是不能在外面被实例化的,但是相比于private他是允许继承的,这里我们就不单独演示了。

#### TypeScript 类的只读属性

对属性成员我们除了可以使用private和protected去控制它的访问级别,我们还可以使用一个叫做readonly的关键词去把这个成员设置为只读的。

这里我们将gender属性设置为readonly,注意这里如果说我们的属性已经有了访问修饰符的话,那readonly应该跟在访问修饰符的后面,对于只读属性,我们可以选择在类型声明的时候直接通过等号的方式去初始化,也可以在构造函数当中去初始化,二者只能选其一。

也就是说我们不能在声明的时候初始化,然后在构造函数中修改它,因为这样的话已经破坏了readonly。

ts
class Person {
public name: string = 'init name';
private age: number;
protected readonly gender: boolean;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
this.gender = true;
}

sayHi(msg: string): void {
    console.log(`I am ${this.name}, ${msg}`);
    console.log(this.age);
}

}

const tom = new Person('tom', 18);

console.log(tom.name);

在初始化过后呢, 这个gender属性就不允许再被修改了,无论是在内部还是外部,他都是不允许修改的。

以上就是readonly这样一个只读属性,还是比较好理解的。

#### TypeScript 类与接口

相比于类,接口的概念要更为抽象一点,我们可以接着之前所说的手机的例子来去做比。我们说手机他是一个类型,这个实例的类型都是能够打电话,发短信的,因为手机这个类的特征呢就是打电话,发短信。

但是呢,我们能够打电话的,不仅仅只有手机,在以前还有比较常见的座机,也能够打电话,但是座机并不属于手机这个类目,而是一个单独的类目,因为他不能够发短信,也不能够拿着到处跑。

那在这种情况下就会出现,不同的类与类之间也会有一些共同的特征,那对于这些公共的特征我们一般会使用接口去抽象,那你可以理解为手机也可以打电话,因为他实现了能够打电话的协议,而座机也能够打电话因为他也实现了这个相同的协议。

那这里所说的协议呢我们在程序当中就叫做接口,当然如果说你是第一次接受这种概念的话,那可能理解起来会有些吃力,个人的经验就是多思考,多从生活的角度去想,如果实在想不通,更粗暴的办法就是不断的去用,用的过程当中慢慢的去总结规律时间长了自然也就好了。

ts
class Person {
eat (food: string): void {}
run (distance: number) {}
}

class Animal {
eat (food: string): void {}
run (distance: number) {}
}

这里我们来看一个例子,我们定义好了两个类型,分别是Person和Animal,也就是人类和动物类,那他们实际上是两个完全不同的类型,但是他们之间也会有一些相同的特性。例如他们都会吃东西都会跑。

那这种情况下就属于不同的类型实现了一个相同的接口,那可能有人会问,我们为什么不给他们之间抽象一个公共的父类,然后把公共的方法都定义到父类当中。

那这个原因也很简单,虽然人和动物都会吃,都会跑,但是我们说人吃东西和狗吃东西能是一样的么,那他们只是都有这样的能力,而这个能力的实现肯定是不一样的。

那在这种情况下我们就可以使用接口去约束这两个类型之间公共的能力,我们定义一个接口叫做EatAndRun,然后我们在这个接口当中分别去添加eat和run这两个方法成员的约束。

那这里我们就需要使用这种函数签名的方式去约束这两个方法的类型,而我们这里是不做具体的方法实现的。

ts
interface EatAndRun {
eat (food: string): void;
run (distance: number): void;
}

那有了这个接口过后呢,我们再到Person类型后面,我们使用implements实现以下这个EatAndRun接口, 那此时在我们这个类型中就必须要有对应的成员,如果没有就会报错,因为我们实现这个接口就必须有他对应的成员。

ts
class Person implements EatAndRun {
eat (food: string): void {}
run (distance: number) {}
}

那这里我们需要注意的一点是,在C#和Java这样的一些语言当中他建议我们尽可能让每一个接口的定义更加简单,更加细化。就是我们EatAndRun这样一个接口当中我们抽象了两个方法,那就相当于抽象了两个能力,那这两个能力必然会同时存在么?是不一定的。

例如说摩托车也会跑,但是就不会吃东西,所以说我们更为合理的就是一个接口只去约束一个能力,然后让一个类型同时去实现多个接口,那这样的话会更加合理一些,那我们这里可以把这个接口拆成一个Eat接口和Run接口,每个接口只有一个成员。

然后我们就可以在类型的后面使用逗号的方式,同时去实现Eat和Run这两个接口。

ts
interface Eat {
eat (food: string): void;
}

interface Run {
run (distance: number): void;
}

class Person implements Eat, Run {
eat (food: string): void {}
run (distance: number) {}
}

那以上呢就是用接口去对类进行一些抽象,那这里再多说一句题外话,就是大家千万不要把自己框死在某一门语言或者是技术上面,最好可以多接触,多学习一些周边的语言或者技术,因为这样的话可以补充你的知识体系。

那最简单来说,一个只了解JavaScript的开发人员,即便说他对JavaScript有多么精通,那他也不可能设计出一些比较高级的产品。

例如我们现在比较主流的一些框架,他们大都采用一些MVVM的这样一些思想,那这些思想呢他实际上最早出现在微软的WPS技术当中的,如果说你有更宽的知识面的话,那你可以更好的把多家的思想融合到一起,所以说我们的视野应该放宽一点。

#### TypeScript 抽象类

最后我们再来了解一下抽象类,那抽象类在某种程度上来说跟接口有点类似,那他也是用来约束子类当中必须要有某一个成员。

但是不同于接口的是,抽象类他可以包含一些具体的实现,而接口他只能够是一个成员的一个抽象,他不包含具体的实现。

那一般比较大的类目我们都建议大家使用抽象类,例如我们刚刚所说的动物类,那其实他就应该是抽象的,因为我们所说的动物他只是一个泛指,他并不够具体,那在他的下面一定会有一些更细化的划分,比如说小狗,小猫之类的。

而且我们在生活当中一般都会说我们买了一条狗,或者说买了一只猫,从来没有人说我们买了一个动物。

ts
abstract class Animal {
eat (food: string): void {}
}

我们有一个Animal类型,那他应该像我们刚刚说的那样,被定义成抽象类,定义抽象类的方式就是在class关键词前面添加一个abstract,那这个类型被定义成抽象类过后,他就只能够被继承,不能够再去实例化。

在这种情况下我们就必须使用子类去继承这个抽象类, 这里我们定义一个叫做Dog的类型, 然后我们让他继承自Animal,那在抽象类当中我们还可以去定义一些抽象方法,那这种抽象方法我们可以使用abstract关键词来修饰,我们这里定义一个叫做run的抽象方法, 需要注意的是抽象方法也不需要方法体.

当父类中有抽象方法时,我们的子类就必须要去实现这个方法。

那此时我们再去使用这个子类所创建的对象时,就会同时拥有父类当中的一些实例方法以及自身所实现的方法。那这就是抽象类的基本使用。

ts
abstract class Animal {
eat (food: string): void {}
abstract run (distance: number): void;
}

class Dog extends Animal {
run (distance: number): void {}
}

关于抽象类呢,更多的还是去理解他的概念,他在使用上并没有什么复杂的地方。

#### TypeScript 泛型

泛型(Generics)是指在定义函数、接口或者类的时候, 不预先指定其类型,而是在使用是手动指定其类型的一种特性。

比如我们需要创建一个函数, 这个函数会返回任何它传入的值。

ts
function identity(arg: any): any {
return arg
}

identity(3) // 3

这代代码编译不会出错,但是存在一个显而易见的缺陷, 就是没有办法约束输出的类型与输入的类型保持一致。

这时,我们可以使用泛型来解决这个问题;

ts
function identity(arg: T): T {
return arg
}

identity(3) // 3

我们在函数名后面加了 <T>, 其中的 T 表示任意输入的类型, 后面的 T 即表示输出的类型,且与输入保持一致。

当然我们也可以在调用时手动指定输入与输出的类型, 如上述函数指定 string 类型:

ts
identity(3) // 3

在泛型函数内部使用类型变量时, 由于事先并不知道它是那种类型, 所以不能随意操作它的属性和方法:

ts
function loggingIdentity(arg: T): T {
console.log(arg.length) // err
return arg
}

上述函数中 类型 T 上不一定存在 length 属性, 所以编译的时候就报错了。

这时,我们可以的对泛型进行约束,对这个函数传入的值约束必须包含 length 的属性, 这就是泛型约束:

ts
interface lengthwise {
length: number
}

function loggingIdentity(arg: T): T {
console.log(arg.length) // err
return arg
}

loggingIdentity({a: 1, length: 1}) // 1
loggingIdentity('str') // 3
loggingIdentity(6) // err 传入是参数中未能包含 length 属性

这样我们就可以通过泛型约束的方法对函数传入的参数进行约束限制。

多个参数时也可以在泛型约束中使用类型参数
如你声明了一个类型参数, 它被另一类型参数所约束。现在想要用属性名从对象里湖区这个属性。并且还需确保这个属性存在于这个对象上, 因此需要咋这两个类型之间使用约束,

简单举例来说: 定义一个函数, 接受两个参数 第一个是个对象 obj,第二个个参数是第一参数 key 是对象里面的键名, 需要输入 obj\[key]

ts
function getProperty(obj: T, key: K) {
return obj[key]
}

let obj = { a: 1, b: 2, c: 3 }

getProperty(obj, 'a') // success
getProperty(obj, 'm') // err obj 中不存在 m 这个参数

我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用

ts
function createArr(length: number, value: T): Array {
let result: T[] = []
for( let i = 0; i < lenght; i++ ) {
result[i] = value
}
return result
}

简单来说,泛型就是把我们定义时不能够明确的类型变成一个参数,让我们在使用的时候再去传递这样一个类型参数。

#### TypeScript 类型声明

在项目开发过程中我们难免会用到一些第三方的npm模块,而这些npm模块他不一定都是通过TypeScript编写的,所以说他所提供的成员呢就不会有强类型的体验。

比如我们这里安装一个lodash的模块,那这个模块当中就提供了很多工具函数,安装完成过后我们回到代码当中,我们使用ES Module的方式import导入这个模块,我们这里导入的时候TypeScript就已经报出了错误,找不到类型声明的文件,我们暂时忽略。

这里我们提取一下camelCase的函数,那这个函数的作用就是把一个字符串转换成驼峰格式,那他的参数应该是一个string,返回值也应该是一个string,但是我们在调用这个函数逇时候并没有任何的类型提示。

ts
import { camelCase } from 'lodash';

const res = camelCase('hello typed');

那在这种情况下我们就需要单独的类型声明,这里我们可以使用detar语句来去声明一下这个函数的类型,具体的语法就是declare function 后面跟上函数签名, 参数类型是input,类型是string,返回值也应该是string

ts
declare function camelCase (input: string ): string;
```

那有了这样一个声明过后,我们再去使用这个camelCase函数。这个时候就会有对应的类型限制了。

那这就是所谓的类型声明,说白了就是一个成员他在定义的时候因为种种原因他没有声明一个明确的类型,然后我们在使用的时候我们可以单独为他再做出一个明确的声明。

那这种用法存在的原因呢,就是为了考虑兼容一些普通的js模块,由于TypeScript的社区非常强大,目前一些比较常用的npm模块都已经提供了对应的声明,我们只需要安装一下它所对应的这个类型声明模块就可以了。

lodash的报错模块已经提示了,告诉我们需要安装一个@types/lodash的模块,那这个模块其实就是我们lodash所对应的类型声明模块,我们可以安装这个模块。安装完成过后,lodash模块就不会报错了。在ts中.d.ts文件都是做类型声明的文件,

除了类型声明模块,现在越来越多的模块已经在内部继承了这种类型的声明文件,很多时候我们都不需要单独安装这种类型声明模块。

例如我们这里安装一个query-string的模块,这个模块的作用就是用来去解析url当中的query-string字符串,那在这个模块当中他就已经包含了类型声明文件。

我们这里直接导入这个模块,我们所导入的这个对象他就直接会有类型约束。

那以上就是对TypeScript当中所使用第三方模块类型声明的介绍。

那这里我们再来总结一下,在TypeScript当中我们去引用第三方模块,如果这个模块当中不包含所对应的类型声明文件,那我们就可以尝试去安装一个所对应的类型声明模块,那这个类型声明模块一般就是@types/模块名。

那如果也没有这样一个对应的类型声明模块,那这种情况下我们就只能自己使用declare语句去声明所对应的模块类型,那对于declare详细的语法这里我们不再单独介绍了,有需要的话可以单独去查询一下官方文档。