TypeScript实战指南(txt+pdf+epub+mobi电子书下载)


发布时间:2020-10-07 13:02:09

点击下载

作者:胡桓铭

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

TypeScript实战指南

TypeScript实战指南试读:

前言

与TypeScript相遇,还是在ThoughtWorks工作的时候。那个时候,我们需要维护大量的前端遗留项目,需要与客户规划我们的人效,需要控制系统迭代带来的bug率。我们接手的项目往往缺乏严谨的注释和完整的代码说明文档,这导致在维护JavaScript遗留项目时,需要花费更多的时间去厘清参数及函数之间的关系,甚至需要用debugger逐层去观察值的变化。工作非常低效,但客户的需求又总是急迫的,这迫使我们去思考如何提升团队的工作效率。

这个时期也是Facebook开始推广Flow的时候。我们觉得添加静态类型应该是个非常不错的方向,也看了很多应用静态类型的成功案例。碰巧Flow对于遗留项目非常友好,你不需要为每个文件、每个函数、每行代码都添加类型,而只需要在你认为有必要的地方写上类型即可。所以我们很快进行了实验。

然而,我们在采用Flow后不久就发现了很多新产生的问题:

1)升级困难,配置复杂。尤其是在React Native项目中,经常会在升级后运行失败。

2)生态弱势。很多第三方库当时没有Flow的类型问题件。

3)难于上手。Flow的气质更像考究的学院派风格,功能强大灵活,但对于新加入团队的人而言,其难度令人生畏。

于是,我们又将目光投向了TypeScript。最初了解TypeScript是看到Angular团队在更新Angular 2时开始全面采用TypeScript代码。他们给出了这样两个理由:

1)TypeScript明确了抽象。在大型工程项目中,我们希望模块之间的边界是使用接口定义的,而JavaScript不足以清晰表达类似的边界划分,Flow也不能。而TypeScript可以定义接口,可以强制程序员去思考API的边界,去设计代码,而不只是编写代码,暴露代码的耦合。

2)TypeScript可以使代码在一定程度上达到“Self-documenting”的效果。“Self-documenting”是一个非常有意思的概念,它强调的是代码本身具有自我说明的效果,而不是依赖文档。TypeScript有着非常严格的强类型表达,这迫使你在函数使用之前就必须标注好函数的入参和返回值类型。这样的强依赖使得函数本身表达清晰,同时也可以非常容易地推导出代码的依赖结构,进行重构。

之后,我们开始尝试在遗留项目中进行TypeScript重构,那是一种相见恨晚的感觉。从后期的数据来看,我们很有效地降低了bug率,同时支持项目的人效也得到了极大的提升。

这一段经历,使我重新开始思考关于语言静态类型的问题。在大型团队开发时,沟通的成本往往是极高的。这就是为什么在后端开发中,拥有静态类型的语言仍然占据主流,也是为什么Python在3.5版本中加入Type Hint。显式的类型声明不仅有利于阅读,也有利于代码编辑器进行代码提示和依赖分析。

比如,在Java开发中,如果需要重构的话,依赖IntelliJ IDEA提供的函数重构功能,可以自动地对每一个依赖该函数的文件进行自动化重构。但这在JavaScript中是不可想象的,你只能使用全局文本搜索来修改函数名,这种操作非常原始,就像在现代战争中还拿着石锤向着敌方阵地冲刺一样。

这就是TypeScript为JavaScript生态带来的价值,也是为什么Angular和Vue都转向使用TypeScript进行重构。比起学术型的Flow而言,TypeScript更像一门工程型的语言,它配置容易,上手快速,更适合在实战中使用,是一件非常称手的“兵器”。

我希望读者在使用TypeScript之前,能够对TypeScript有足够的了解。我结合TypeScript的官方手册与其他公开资料,整理了一些TypeScript基础的内容,就是本书的“

基础篇

”,最好粗略过一遍这部分内容。在“实战篇”中会提及这些内容,返回去再看时,反而能加深理解。

实际上,如何在实战中使用TypeScript反而是一个老大难的问题。这也是初学者更容易遇到的困难。“为什么手册读完了,官方实例也看了,我还是不会在React里写TypeScript呢?”这是我经常听到的反馈,希望本书能够很好地回答这类问题。

最后,非常感谢2018年年底的住院经历,因为无法完全被治愈,使得我开始重新思考生命与健康的问题,如果有机会我也非常想聊聊这个话题。我非常感谢娜娜的陪伴,这是最长情的告白。同时感谢吴怡编辑的理解与体谅,使得我还有机会完成此书。最后感谢开源社区,不仅帮助我成长,也提供了丰富的资料助力我完成此书,希望能有更多的机会回馈社区。作者于2019年元宵节基础篇■第1章 Hello TypeScript■第2章 类型与函数■第3章 接口与类■第4章 命名空间与模块第1章 Hello TypeScript

对于大部分前端开发者而言,TypeScript是一门新鲜且陌生的语言。既然已经有JavaScript了,为什么还需要TypeScript呢?TypeScript与JavaScript又有什么不同呢?

从某种严格的角度来讲,人们并不需要重复的东西,人们需要不一样的、特别的东西,就像两位面试者站在面试官前,都努力地表现出自己的差异性。

本章通过回溯JavaScript的发展历史来讲述TypeScript的差异性,希望通过梳理TypeScript的发展脉络,让读者了解只属于TypeScript的特性。

准备好了吗?就从Hello TypeScript开始吧。1.1 引言

人们在使用一门语言之前需要对其进行多方“面试”。

你很难想象现在的前端世界竞争有多么激烈,除了长年稳坐第一的JavaScript以外,还有CoffeeScript、Dart、ClojureScript,等等,甚至“新人”Kotlin也希望来蹭一下热度,分一杯羹。

因此,非常有必要先来看一下TypeScript的“个人简历”:·JavaScript的超集。·支持ECMAScript 6标准,并支持输出ECMAScript 3/5/6标准的纯JavaScript代码。·支持ECMAScript未来提案中的特性,比如异步功能和装饰器。·支持类型系统且拥有类型推断。·支持运行在任何浏览器、Node.js环境中。

从这份简历可以了解到,TypeScript与JavaScript、ECMAScript有着非常深入的联系。那么在谈论TypeScript之前,有必要对JavaScript与ECMAScript做一次充分的背景调查。也只有在了解了JavaScript与ECMAScript之后,我们才更有资格去谈论TypeScript是否适合我们。1.1.1 JavaScript与ECMAScript

JavaScript是蹭热点的“网红”出身。在1995年,它被搭载在网景浏览器中首次发布,当时名字还是LiveScript。由于网景觉得这个名字缺乏热度,所以决定蹭一下流行的Java的热度,最终改名叫JavaScript。

如果非要说JavaScript与Java有什么关系的话,大概就相当于雷锋与雷峰塔的关系了。在国外程序员圈内,则喜欢用HAM(火腿)和HAMSTER(仓鼠)来比喻两者的关系,如图1-1所示。

JavaScript的成功引起了微软的注意,其在IE 3.0上搭载了JScript。JScript也是一种JavaScript的实现,两种JavaScript语言的出现意味着浏览器端语言标准化的缺失,甚至可能进一步出现分裂状态。图1-1 Java与JavaScript的关系就像HAM之与HAMSTER之间的关系

所以在1996年,网景将JavaScript提交给ECMA International(欧洲计算机制造商协会)进行标准化,最终确定了新的语言标准,取名为ECMAScript。

从此,所有JavaScript的实现都必须以ECMAScript标准为基础。但由于JavaScript的历史原因,我们仍然用JavaScript称呼这门语言,而用ECMAScript称呼标准。

事情在开端的时候总是美妙的。

1997年ECMAScript发布了首版标准,紧接着1998年6月发布第二版标准。但在1999年12月发布了第三版标准之后,不幸开始降临了。在接下来的10年里,ECMAScript再也没能为标准化做出太大的贡献,甚至不同浏览器中的实现与标准大相径庭。不仅如此,各大厂商也开始向自己的JavaScript里添加“私货”,比如JScript的ActiveXObject。

这10年里究竟发生了什么呢?比较公允的看法是由于ECMAScript 4过分激进的草案导致了浏览器厂商的一致抵制。IE和Flash在这一时期的强势也导致了ECMAScript的进一步没落。

直到2005年秋,Task Group 1 of Ecma Technical Committee 39(TG1)才开始定期召开会议。之后,大火的Ajax让人们意识到了JavaScript的复兴,标准化工作才开始加速。

经过一系列复杂的争论,2009年12月ECMAScript 5得以发布。随后的2012年,国外的开发者社区推动停止对旧版本IE的支持工作,使得ECMAScript 5开始流行。

2015年,ECMAScript规范草案的委员会TC39决定将定义新标准的制度改为一年一次。这意味着ECMAScript的更新不再依赖于整个草案的完成度,而可以根据添加的特性进行滚动发布。

同年,代号为Harmony的ECMAScript 6,也就是耳熟能详的ES 6,或者叫ES 2015,得以发布。Harmony(和谐)这个名字很有意思,仿佛在告诉开发者这么多年的争执与混乱终于平息。

但现在浏览器又开始拖后腿了。新特性往往很难在第一时间得到浏览器的支持,所以这一时期诞生了大量的前端工具,使开发者可以在开发环境中提前使用ECMAScript已发布或者还是草案的新特性。比如,Babel通过插件化的方式引入ECMAScript的特性,并在生产环境时编译到ES 3或ES 5的代码。1.1.2 TypeScript

TypeScript是在新时代诞生的。

Ajax的火热和JavaScript的复兴标志着一个全新时代的到来。这个时期的JavaScript代码正在变得越来越庞大,构建规模化JavaScript应用程序的需求日益旺盛。

微软的语言开发者发现,内部的研发部门以及外部的客户都表示JavaScript大型Web应用很容易出现失控,变得难以驾驭。而类似CoffeeScript和Script#的语言又难以使用JavaScript的语言特性。

微软认为JavaScript只是一门脚本语言,设计理念简单,缺乏对类与模块的支持,并非真正用于开发大型Web应用。这使得微软内部开始出现需要自定义工具去强化JavaScript开发的需求。

2012年10月,Delphi、C#之父安德斯·海尔斯伯格主持开发的TypeScript终于发布,并且他亲自进行推广。

TypeScript的主要特点如下:

1)免费开源,使用Apache授权协议。

2)基于ECMAScript标准进行拓展,是JavaScript的超集。

3)添加了可选静态类型、类和模块。

4)可以编译为可读的、符合ECMAScript规范的JavaScript。

5)成为一款跨平台的工具,支持所有的浏览器、主机和操作系统。

6)保证可以与JavaScript代码一起运行,无须修改。(这一点保证了JavaScript项目可以向TypeScript平滑迁移。)

7)文件拓展名是ts。

8)编译时检查,不污染运行时。

可以说,TypeScript的设计充满了克制风格,这也使得它在诞生初期并没有迎来太大的反响。但安德斯·海尔斯伯格毕竟是语言大师,我们可以看一下自2012年后TypeScript的热度趋势图,如图1-2所示。

从与ES 6同期发布开始,TypeScript的热度开始了前所未有的爆发。这是因为,同时期大规模的单页应用需求开始井喷,也让市场意识到了TypeScript的重要性,这就是大师的前瞻性了。

与Facebook发布的静态类型检测工具Flow及其他语言相比的热度趋势图如图1-3所示。

如今,TypeScript成为微软发展的重点项目,其将新版本的发布节奏加快到了平均一个半月一次。在这样高强度的更新下,我们甚至可以期待将来TypeScript支持WebAssembly。图1-2 TypeScript热度趋势图(2012年11月至2017年11月)图1-3 热度趋势图(2014年10月至2017年12月)1.2 准备环境

从现在起,我们就可以忘掉JavaScript、ECMAScript了,把思绪放空,回到眼前的TypeScript来。聊了这么多,是时候跟TypeScript打个招呼了,但在正式打招呼之前,我们还有些准备工作要做。1.2.1 安装Node.js

Node.js是JavaScript的一个基于服务端的运行环境,大部分JavaScript工具链都需要它才能运行,TypeScript也不例外,所以下面先介绍Node.js。

首先打开Node.js的中文官网:

https://nodejs.org/zh-cn/

此时会有两个下载版本可以选择,如图1-4所示。图1-4 Node.js中文官网

带LTS标记的版本意味着该版本会有长达30个月的官方维护期,包括安全问题、文档更新等;而不带此标记的版本会在新版本发布后仅拥有2个月的维护期。

所以,如果是在生产环境中使用,尽量使用LTS版本。如果只是想学习Node.js的话,可以根据版本的不同特性进行选择。而在本书中,推荐安装Node.js 8.x。Node.js的安装包在各个系统上的差异不大,所以下载完成之后,直接打开安装就可以了。

如果使用Windows,在安装完成之后,可以进入CMD(同时按住Win和R键,然后输入CMD,再按回车可进入),在CMD中输入:node -v

这时,如果出现类似如下的版本提示,则说明安装成功:node -v v8.11.1

同理,如果使用Linux和Mac,可以打开终端,然后输入同样的命令,也能看到版本号。1.2.2 npm和Yarn

npm的全称是Node Package Manager,翻译过来就是Node.js的包管理工具。它不仅承接了Node生态的包管理,也承接了前端JavaScript的包管理工作,同时它还与Node.js一起无痕地捆绑安装,不需要再次去搜索下载,这使得npm很快得到了普及。

npm由三个不同的部分组成:网站、注册表和CLI。网站是用户发现软件包的主要工具,注册表是一个关于软件包信息的大型数据库,而CLI则告诉开发者如何在注册表上发布软件包或下载软件包。

那Yarn是什么呢?Yarn是Facebook、Google、Exponent和Tilde共同开发的一款新JavaScript包管理工具。它并没有试图完全取代npm。Yarn同样是从npm注册源获取模块的CLI客户端。注册的方式不会有任何变化,即同样可以正常获取与发布包。它存在的目的是解决团队使用npm面临的少数问题,比如依赖版本的锁定、并行安装以及文案输出等。当然,在Node版本的更替中,npm本身也在积极更新并解决这些问题。

但在本书中,我们使用Yarn作为项目的包管理工具。

可以通过以下方式安装Yarn:npm install –g yarn

当然,Yarn的官方文档有明确说明。一般来说,不推荐通过npm安装Yarn,在用基于Node.js的包管理工具安装Yarn时,该包未被签名,并且只通过基本的SHA1散列进行唯一完整性检查。这在安装系统级应用时有安全风险。

为此,建议访问以下网址,采用Yarn官方推荐的方式进行安装:

https://yarnpkg.com/zh-Hans/docs/install#mac-tab1.2.3 安装TypeScript

TypeScript的安装非常简单,只需执行如下命令即可:npm install –g typescript

但通常在实际项目中,我们不会对TypeScript进行全局安装。因为TypeScript自带的tsc命令并不能直接运行TypeScript代码,所以通常我们还会安装TypeScript的运行时ts-node:npm install –g ts-node

ts-node并不等于TypeScript的Node.js,仅仅封装了TypeScript编译的过程,提供直接运行TypeScript代码的能力。1.3 Visual Studio Code

似乎我们忘了点什么,没错,那就是一款适合TypeScript的IDE。

工欲善其事,必先利其器,这说明需要挑选一款得心应手的兵器。而推荐Visual Studio Code(以下简称VSCode)的原因主要有以下三个:

1)免费开源,可跨平台使用且跨平台体验非常一致。

2)同样由微软主导开发,并进行高频率的持续更新。

3)几乎由TypeScript实现,天然对TypeScript有良好的支持。当光标悬停在变量上时,会主动提示变量类型,非常方便。1.3.1 安装VSCode

访问VSCode的官网,可以直接根据当前系统版本下载需要的安装包,如图1-5所示。

https://code.visualstudio.com/图1-5 VSCode官网下载页面

VSCode会自动检查是否为最新版本,并提示用户自动更新,所以无须担心当前是否为最新版本。1.3.2 安装Shell命令

按住快捷键P(Windows,Linux使用Ctrl+Shift+P),在浮窗中输入:install 'code'

如图1-6所示,选择“Shell Command:Install'code'command in PATH”后,会提示已经成功安装Shell命令。之后在终端中使用code命令就可以快速打开文件或者文件夹,比如code test.js或者code./folder。图1-6 选择安装Shell命令1.4 Hello World

好的,经历了一番波折后,我们终于来到了经典的Hello World环节。

想必你已经有点不耐烦了,但我还是得啰嗦一下步骤,打开VSCode,新建文件,名叫hello.ts。

这个时候让我们忘记各种概念,ES的历史也好,TypeScript也好,让它们都化作粉尘吧。

请回忆你记忆中的JavaScript,如果要输出一行Hello World该如何操作?难道不是下面这样吗?console.log('hello world');

当然是这样,TypeScript与JavaScript的基础语法几乎一样。

但我们还是需要谨慎验证一番。按住快捷键J(Windows和Linux为Ctrl+J),此时会打开VSCode自带的终端,输入ts-node hello.ts,如图1-7所示。图1-7 Hello World

此时,你会看到一个漂亮的Hello World的输出。

但这个漂亮的输出不禁让你陷入深深的沉思:“有啥不一样?”TypeScript与JavaScript到底有什么不同呢?还真的有很多不同,让我们去下一章看看类型与函数。1.5 本章小结

经过一番严格的“考核”,我们了解到TypeScript不仅出身“名门”微软,还有着日益增长的“声望”。它不仅能完全适用于JavaScript开发,还带来了非常多新的特性。

但不要被它迷惑了!对,我说的就是你!

不能简单地被TypeScript迷惑了,这一次我可以给它通过面试。但在接下来的章节里,我们必须给它一些艰巨的任务,让它充分展现自己的实力,让我们了解一下为什么它能在混战的前端业界占有一席之地。1.6 作业

1.我们都知道IDE中拓展的威力,好的拓展能够让我们事半功倍,能自动纠错,能辅助我们写代码,所以尝试在VSCode中安装与TypeScript的开发相关的拓展。

2.快捷键在IDE中有着不可动摇的重要地位,熟悉快捷键能够提升你的代码编写速度,就像设计师如果熟练掌握快捷键就能熟练地P图一样,请尝试熟记VSCode的快捷键。

3.漂亮的开发环境总是能令我们身心愉悦,请尝试修改VSCode的配色和文件图标。

4.除了TypeScript,现在知名的静态类型检测器还有Flow。尝试了解Flow的产生和发展,以及与TypeScript的不同之处。这会是一件非常有趣的事,在了解完之后你会惊讶地发现,TypeScript的社区生态比Flow的更壮大,这算是微软与Facebook比拼的一次大胜利吧。第2章 类型与函数

有人认为TypeScript=Typed JavaScript,这是一个非常有趣的说法。说明TypeScript的核心就是Type(类型)。

在本章中,我们将先学习TypeScript的核心类型系统,同时还会学习它与JavaScript类似的一面,也就是函数系统。

从两大最基本的知识点入手,可展示出TypeScript的巨大魅力。2.1 基本类型

在本节中,我们将先回顾一下JavaScript的七种基本类型。因为TypeScript是JavaScript的超集,所以TypeScript的基本类型与JavaScript是完全相同的。同时,为了便于学习,本节还介绍了泛型、枚举、symbol、iterator及generator等。2.1.1 JavaScript的基本类型

当我们开始深入TypeScript的类型系统之前,需要先回顾一下JavaScript的数据类型。

JavaScript语言的数据类型包括以下7种:·boolean(布尔值),也就是通常所用的true和false。·null,一个表明null值的特殊关键字。JavaScript是大小写敏感的,不能误写成Null或者NULL。·undefined,变量未定义时的属性。·number,表示数字,例如:1或者1.2。·string,表示字符串,例如:“Hello TypeScript”。·symbol一种数据类型(在ECMAScript6中新添加的类型),它的实例是唯一且不可改变的,我们会在后面单独讲解。·object(对象),通常而言,可以将对象视为存放值的命名容器。

以上类型,都是在运行时进行处理,我们并不能在代码阶段获知类型,比如:function append(a, b) { return a + b;}

从上面代码中,我们无法得知a和b究竟是字符串还是数字。虽然两者都不会影响程序的运行,但如果代码变得更为复杂,缺乏类型约束可能会导致一些潜在的隐患。

所以让我们来看看TypeScript的数据类型,从形式上看,与JavaScript是相同的。2.1.2 TypeScript的基本类型

作为JavaScript的超集,TypeScript支持与JavaScript几乎相同的数据类型。

首先来看一下boolean类型:let areYouOk: boolean = true

当我们想声明数字的时候,可以这样写:let a: number 6let b: number = 1_000_000

和JavaScript一样,可以使用双引号(")或单引号(")表示字符串:let name: string = "xiaoming";let otherName: string = "xiaohong";

还可以使用模板字符串,它可以定义多行文本和内嵌表达式。这种字符串是由反引号包围(`),并且使用$向句子中插入表达式:let name: string = `xiaoming`;let age: number = 37;let sentence: string = `Hello, my name is ${ name }.I'll be ${ age + 1 } years old next month.`;

这与下面定义sentence的方式效果相同:let sentence: string = "Hello, my name is " + name + ".\n\n" + "I'll be " + (age + 1) + " years old next month.";

undefined和null的定义方式也同样。

在TypeScript中定义数组有两种方式,第一种方式是可以在元素类型后面接上[],表示由此类型元素组成的一个数组:let list: number[] = [1, 2, 3];

第二种方式是使用数组泛型,Array<元素类型>:let list: Array = [1, 2, 3];

在这里,你可能会问,为什么每个变量的声明后,都必须加上变量类型呢?

其实这不是必须的,我们可以来看一看如何在TypeScript中声明变量。2.1.3 变量声明

let和const是ES 6中新增的变量声明方式。在TypeScript中,let和const的使用方式与ES 6中一模一样,在本书中也推荐使用let和const进行赋值,并不推荐使用var去进行赋值。var声明有很多怪异的地方,有兴趣的读者可以自行了解。

回到变量声明,即便不指定类型,TypeScript依旧可以根据指定的变量进行类型推断,比如:let name = `xiaoming`;// let a: stringconst age = 5// const age: 5

如果这个时候为name赋值1,则会得到一个报错信息:let name = `xiaoming`;name = 1// error TS2322: Type '1' is not assignable to type 'string'.

从这个例子可以得出,如果在初始化阶段已经声明了该变量类型,在中途更改,会触发TypeScript的编译时报错。这个编译时报错为我们带来了严格的类型检查。

但常常我们会遇到另外一种情况,你会比TypeScript更了解这个值的类型。这样的情况经常发生,你会清楚地知道编译器的类型推断不太确切,应该有更准确的类型。

而通过类型断言,我们可以明确地告知编译器,我们究竟想干什么。这是一种修正方案,在别的语言里,我们将会使用类型转换来表达这样一种情况。

但这个操作并不会在运行时里起到任何影响,正如TypeScript的宣言一样,它仅仅在编译时起作用。这非常像Word的语法检查,是提醒和报错,而不会阻挡你继续写字。

类型断言分作两种形式,第一种用“尖括号”:let oneString: any = "this is a string";let stringLength: number = (oneString).length;

这也是大部分编程语言中常用的语法形式。

而另外一种则使用关键字as:let oneString: any = "this is a string";let stringLength: number = (stringLength as string).length;

两种形式是等效的,至于具体使用哪一种,则取决于个人的偏好。

不过值得注意的是,在TypeScript的JSX中类型断言可以使用as,而不允许使用尖括号方式。如果非要有什么原因的话,大概是由于尖括号在JSX中已经用于表达泛型了。2.1.4 泛型

泛型对于纯JavaScript程序员而言可能有点难以理解。

泛型用于提升代码的重用性。我们希望自己编写的代码,无论是模块还是组件,不仅能支持当前设计的数据类型,而且也能支持将来的数据类型。这在大型系统中是非常基础且重要的功能。所以我们常常能在各种各样的静态类型语言中看到泛型设计,使得用户可以灵活地选择希望支持的类型,而非局限于某一种类型。

1.泛型函数

下面我们来创建一个使用泛型的例子:hello函数。这个函数非常简单,直接将输入的参数作为它的返回值。

不过,在这之前,我们不妨来假设一下,如果不使用泛型,会是什么样子。

比如,当我们需要传入一个数字的时候是如下这样:function hello(arg: number): number { return arg;}

但是很不幸,我们的需求很快就变更了,这个时候需要传入一个字符串,就会变成如下这样:function hello(arg: string): string { return arg;}

当然我们也可以使用any类型来表达这种混沌感:function hello(arg: any): any { return arg;}

但any类型并不能准确地表达返回值与参数必须是相同类型,因为any代表任何类型,这使得我们的类型表达开始混乱了。

因此,我们需要一种表达方式来控制函数的参数类型,这就要用到泛型了。

TypeScript的泛型语法非常主流,与Java等静态类型语言的使用方式一致,就像下面这样:function hello(arg: T): T { return arg;}

我们给hello函数添加了泛型变量T,T代表用户即将传入的类型。类型既可以是number,也可以是string,而在最后,我们还使用T作为返回值的类型。这就达到了返回值和参数类型相同的目的,保持住了函数表达的准确性。

那如何使用泛型函数呢?跟之前的类型断言一样,有两种方法可以选择:·使用熟悉的尖括号方式进行表达:let output = hello("hello TypeScript");

我们在这里明确地指定T是string类型,并将它作为参数传给函数。当然,如果我们需要使用number类型,就直接在尖括号中写入number即可。·使用类型推断。TypeScript的编译器会根据传入的参数类型自动确定T的类型。这看上去非常智能,写起来是如下这样的:let output = hello("hello TypeScript");

你什么都不用做,像平常一样写代码即可。

这个时候编译器会明确地知道T的类型就是参数"hello TypeScript"的类型string,同样,返回值类型是T,也是string类型。

如果这个时候你使用的是VSCode,将光标移至output上就会看到string的类型提示,如果再移动到hello头上,可以看到它的参数类型也写着string。

类型推断帮助我们精简了代码,也提高了可读性,让代码变得简洁。在后续的章节中,我们还会体会到类型的好处。但凡事皆有例外,在某些特别复杂的情况下,类型推断是会失灵的,那么这个时候就需要我们非常明确地写出T的类型。

2.泛型变量

创建hello这样的泛型函数时,编译器要求在函数体中正确地使用类型。这听起来像是一句废话,换言之,你必须把这些参数当成任意类型。

让我们回顾一下hello这个函数:function hello(arg: T): T { return arg;}

如果我们这个时候需要使用参数arg的长度时,就会出现下面这样尴尬的情况:function hello(arg: T): T { console.log(arg.length); // Error: T doesn't have .length return arg;}

编译器会非常迅速地进行报错,告诉我们泛型T并没有length这个属性。这似乎有点不合情理,如果T是string类型,那它为什么没有length属性呢。万一T是number类型呢?就像木桶原理一样,编译器会选择最糟糕的情况进行处理,T代表任意类型,那么就一定会有最糟糕的情况。

什么情况下一定会有length属性呢?可以使用泛型数组来表达这样的情况。由于我们操作的是数组,所以length属性一定是存在的,那就可以像普通的数组一样操作它:function hello(args: T[]): T[] { console.log(args.length); return arg;}

这个时候,我们再使用hello函数时,就需要传入一个T的数组,也就是一个string变量的数组,或者number变量的数组,而不是一个单纯的变量。返回值也是同类型的数组。这可以让我们把泛型变量T作为数组的一部分属性,而不是作为整体类型,这增加了灵活性。

不只是使用中括号,还可以使用Array来表达数组,如下所示:function hello(arg: Array): Array { console.log(arg.length); return arg;}

TypeScript同时采用了这样一种在其他语言中非常常见的语法。2.1.5 枚举

我们常常会有这样的场景。比如与后端开发约定订单的状态开始是0,未结账是1,运输中是2,运输完成是3,已收货是4。这样的纯数字会使得代码缺乏可读性。枚举就用于这样的场景。枚举可以让我们定义一些名字有意义的常量。使用枚举可以清晰地表达我们的意图。TypeScript支持基于数字枚举和字符串的枚举,下面分别介绍。

1.数字枚举

首先我们举例来看数字枚举:enum OrderStatus{ Start= 1, Unpaid, Shipping, Shipped, Complete,}

就像上面这样,我们通过数字来表达了订单状态。在实际的代码编写时,我们就直接使用OrderStatus.Start来代替原本的数字1,这就使得代码具备了相当的可读性,甚至可以免去冗余的注释。

在上面的代码中还使用了一个小技巧,当只写Start=1时,后面的枚举变量就是递增的。但你也可以明确地写出每个枚举变量的数字,这取决于具体的业务场景。

如果我们连第一个枚举的变量的值都不写,会怎么样呢?就像下面这样:enum OrderStatus{ Start, Unpaid, Shipping, Shipped, Complete,}

现在,Start的值就是0了,后面的枚举类型再依次递增。

通常情况下,在使用这样的写法时,我们其实是不在乎成员变量的具体值的,我们只知道这些值是不同的。

当然枚举类型中的值必须是确定的,比如,像下面这样的写法是不允许的:enum Example { A = hello(), B, // error! 'A' is not constant-initialized, so 'B' needs an initializer}

类似这样没有给出确定值的写法,在TypeScript中都是不允许的。

2.字符串枚举

字符串枚举的概念与数字枚举有一些细微的差别。

在一个字符串枚举中,所有成员必须都是字符串字面量,如下所示:enum OrderStatus{ Start='Start', Unpaid='Unpaid', Shipping='Shipping', Shipped='Shipped', Complete='Complete',}

由于字符串枚举没有递增的含义,字符串枚举成员都必须手动初始化。虽然显得烦琐,但相比在运行时环境下不能表达有用信息的数字枚举,字符串枚举能进一步提升可读性,且在运行时中提供具有刻度性质的值,这使得调试变得更容易。所以在实际的开发中,大家更喜欢使用字符串枚举。

3.反向映射

反向影射是数字枚举的一个技巧,这得益于TypeScript在实现数字枚举时的代码编译。

比如当我们设定了如下一个枚举时:enum Enum { A}

编译器拿到这段代码会编译到JavaScript,是如下这样的:(function (Enum) { Enum[Enum["A"] = 0] = "A";})(Enum || (Enum = {}));

所以我们既可以从属性名获取值,也可以从值获取属性名:const = Enum.A; // 0constnameOfA = Enum[a]; // "A"

当然,这个特性是因为编译成JavaScript后是这样,所以在运行时包含了正向映射(key->value)和反向映射(value->key)。而字符串枚举编译后并没有这样的特性。

所以要注意,在字符串枚举中没有反向映射。2.1.6 symbol

自ES 6起,symbol成为一种新的原生类型,就像基本类型number和string一样。

TypeScript中使用symbol类型如出一辙,也是通过Symbol构造函数创建的,如下所示:const symbol1 = Symbol();const symbol2 = Symbol("hello");const symbol3 = Symbol("hello");symbol2 === symbol3; // false

通过同样的方式生成两个symbol,也是不同的,因为symbol是唯一的。所以symbol2和symbol3无论如何都不会相等。

像字符串一样,symbol也可以用于对象属性的键:const symbol = Symbol();const obj = { [symbol ]: "value"};console.log(obj[symbol]); // "value"

在实际开发中,常量使用symbol值最大的好处就是,其他任何值都不可能有相同的值了,因此可以保证诸如特定字面量或者特定的switch语句值可以按设计的方式工作。2.1.7 iterator和generator

1.iterator

当一个对象实现了Symbol.iterator时,我们认为它是可迭代的。如array、map、set、string、int32Array、uint32Array等一些内置的类型,目前都已经实现了各自的Symbol.iterator。对象上的Symbol.iterator函数负责返回供迭代的值。

for..of语句会遍历可迭代的对象,调用对象上的Symbol.iterator方法。

比如下面是在数组上使用for..of的例子:const array = [233, "hello", true];for (let value of array) { console.log(value); // 233, "hello", true}

for..of和for..in都可以迭代一个数组,但它们之间的区别很大。最明显的区别莫过于它们用于迭代器的返回值并不相同,for..in迭代的是对象的键,而for..of迭代的是对象的值。

我们可以从下面的例子中看出两者之间的区别:const array = [3, 4, 5];for (let i in array) { console.log(i); // 0, 1, 2}for (let i of array) { console.log(i); // 4, 5, 6}

另一个区别在于,for..in可以操作任何对象,提供了查看对象属性的一种方法。但是for..of关注迭代对象的值,内置对象Map和Set已经实现了Symbol.iterator方法,让我们可以访问它们的值:const fruits = new Set(["apple", "pear", "mango"]);fruits["peach"] = "Princess Peach! Make a wish!";for (let fruit in fruits) { console.log(fruit); // "peach"}for (let fruit of fruits) { console.log(fruit); // "apple", "pear", "peach"}

但这样的特性仅仅在ES 6及以上才上生效。

当我们将TypeScript的代码生成目标设定为ES5或ES3,迭代器就只允许在array类型上使用。在非数组值上使用for..of语句会得到一个错误。即便这些非数组值已经实现了Symbol.iterator属性,也是不可以的。

编译器会生成一个简单的for循环作为for..of循环,比如:const numbers = [1, 2, 3];for (let number of numbers) { console.log(number);}

生成的代码为:var numbers = [1, 2, 3];for (var _i = 0; _i < numbers.length; _i++) { var number = numbers[_i]; console.log(number);}

2.generator

function*是用来创建generator函数的语法。(在MDN的文档中generator称为生成器。)

调用generator函数时会返回一个generator对象。generator对象遵循迭代器接口,即通常所见到的next、return和throw函数。

generator函数用于创建懒迭代器,例如下面的这个函数可以返回一个无限整数的列表:function* infiniteList() { let i = 0; while(true) { yield i++; }}var iterator = infiniteList();while (true) { console.log(iterator.next()); // { value: xxxx, done: false }}

当然,也可以设定某个条件终止它,而不只是永远循环下去。如下所示:function* infiniteList(){ let i = 0; while(i < 3) yield i++;}let gen = infiniteList();console.log(gen.next()); // { value: 0, done: false }console.log(gen.next()); // { value: 1, done: false }console.log(gen.next()); // { value: 2, done: false }console.log(gen.next()); // { value: undefined, done: true }

可以说这个设定是generator中最令人兴奋的部分。它在实质上允许一个函数可以暂停执行,比如当我们执行了第一次的gen.next()后,可以先去做别的事,再回来继续执行gen.next(),这样剩余函数的控制权就交给了调用者。

当你直接调用generator函数时,它并不会执行,它只会创建一个generator对象。

在下面的例子中,我们可以看到一个更灵活的使用方式:function* generator(){ console.log('Execution started'); yield 0; console.log('Execution resumed'); yield 1; console.log('Execution end');}

执行它,则会看到如下输出结果:constiterator = generator();console.log(iterator.next()); // "Execution started"// { value: 0, done: false }console.log(iterator.next()); // { value: 1, done: false }// "Execution resumed'"// { value: 1, done: false }console.log(iterator.next()); // "Execution end"// { value: undefined, done: true }console.log(iterator.next()); // { value: undefined, done: true }

从上面代码可以得知:·generator对象只会在调用next时开始执行。·函数在执行到yield语句时会暂停并返回yield的值。·函数在next被调用时继续恢复执行。

所以实质上generator函数的执行与否是由外部的generator对象控制的。

不过除了yield传值到外部,我们也可以通过next传值到内部进行调用。下面的例子展示了iterator.next传值的方式:function* generator() { const who = yield; console.log('hello '+ who); // bar!}const iterator = generator();Console.log(iterator.next());// {value: undefined, done: false}console.log(iterator.next('TypeScript')); // hello TypeScript// {value: undefined, done: true}

以上便是next和return函数的内容,接下来我们来看一下throw函数如何处理迭代器内部报错。

下面是iterator.throw的例子:function* generator() { try { yield 1; } catch(error) { console.log(error.message); }}const iterator = generator();iterator.next()// {value: 1, done: false}iterator.throw(new Error('something incorrect'));// something incorrect// {value: undefined, done: true}

通过以上的案例我们可以得知,外部是可以对generator内部进行干涉的:·外部系统可以传递一个值到generator函数体中。·外部系统可以抛入一个异常到generator函数体中。2.2 高级类型

上一节介绍的TypeScript类型,除了泛型以外,都在JavaScript中有同样的定义和应用,而本节介绍的高级类型则完完全全属于TypeScript独有的。我们需要了解如何进行类型保护、类型区分以及类型推导,这对于高效使用TypeScript非常有帮助。

另外,interface关键字可以用于描述对象的结构,为了复用对象结构,自然会催生出交叉类型和联合类型等需求,那么,interface便大有用场。2.2.1 interface

一个很常用的场景,比如函数传参,除了基本类型和数组以外,我们通常喜欢使用字典作为参数,那该如何对字典进行类型约束呢?TypeScript引入了interface关键字,为我们提供了表达字典的能力,如下面的例子所示:interface A { a: number, b: string, c: number[]}let a: Aa.a = 1a.b = 'hello'a.c = [1, 2 ,3]a.d // [ts] Property 'd' does not exist on type 'A'.

表达字典的类型是interface最常用的场景,除此以外,interface作为接口的能力将在下一章详细讲述。2.2.2 交叉类型与联合类型

通常意义上,我们所说的交叉类型是指将多个字典类型合并为一个新的字典类型。

基本类型是不会存在交叉的。比如number和string是不可能有交叉点的,一个类型不可能既是字符串又是数字。所以当我们使用交叉类型时通常是下面这样:type newType = number & string;let a: newType;interface A { d: number, z: string,}interface B { f: string, g: string,}type C = A & Blet c: C

这里的type关键字是用来声明类型变量的。在运行时,与类型相关的代码都会被移除掉,并不会影响到JavaScript的执行。

但如果交叉类型中有属性冲突时,比如下列代码:let a: newType;interface A { d: number, z: string,}interface B { d: string, g: string,}type C = A & Blet c: Cc.d = 1;// [ts]// Type '1' is not assignable to type 'number & string'.// Type '1' is not assignable to type 'string'.// (property) d: number & stringc.d = "123";// [ts]// Type '"123"' is not assignable to type 'number & string'.// Type '"123"' is not assignable to type 'number'.// (property) d: number & string

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载