大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全家桶1年46,售后保障稳定
TypeScript超详细入门教程(上)
01 开篇词:Hello~TypeScript
01 开篇词:Hello~TypeScript
更新时间:2019-10-30 13:49:46
既然我已经踏上这条道路,那么,任何东西都不应妨碍我沿着这条路走下去。——康德
同学你好,我是Lison。很高兴你对TypeScript感兴趣,或许你对TypeScript了解还不多,或许还有很多疑问,比如:
-
学 TypeScript 是不是就不需要学 JavaScript 了?
-
Vue 用 TypeScript 改写发布 3.0 后是不是不用 TypeScript 不行?
-
TypeScript 靠谱吗?
诸如此类疑惑,导致你一直对它犹豫不决,那么本节我将代替 TypeScript 向你做一个自我介绍。
同学你好,我是 TypeScript,如果你觉得我是 JavaScript 的孪生兄弟,或者觉得我是前端圈新扶持起来的太子,那你可能对我是有点误解了。其实我并不是一个新的语言,用大家公认的说法,我是JavaScript的超集,你可以理解为,我是加了一身装备铭文的进化版 JavaScript。JavaScript 有的,我都有,而且做得更好。JavaScript 没有的,我也有,而且我是在很长一段时间内不会被 JavaScript 赶上的。
虽然我作为超集,但是我始终紧跟 ECMAScript 标准,所以 ES6/7/8/9 等新语法标准我都是支持的,而且我还在语言层面上,对一些语法进行拓展。比如新增了枚举(Enum)这种在一些语言中常见的数据类型,对类(Class)实现了一些ES6标准中没有确定的语法标准等等。
如果你是一个追赶技术潮流的开发者,那你应该已经将 ES6/7/8/9 语法用于开发中了。但是要想让具有新特性的代码顺利运行在非现代浏览器,需要借助Babel这种编译工具,将代码转为ES3/5版本。而我,可以完全不用 Babel,就能将你的代码编译为指定版本标准的代码。这一点,我可以说和 JavaScript 打了个平手。
另外我的优势,想必你也略有耳闻了那就是我强大的类型系统。这也是为什么造世主给我起名TypeScript。如果你是一名前端开发者,或者使用过 JavaScript 进行开发,那么你应该知道,JavaScript 是在运行的时候,才能发现一些错误的,比如:
-
访问了一个对象没有的属性;
-
调用一个函数却少传了参数;
-
函数的返回值是个字符串你却把它当数值用了;
-
…
这些问题在我这里都不算事。我强大的类型系统可以在你编写代码的时候,就检测出你的这些小粗心。先来简单看下我工作的样子:
interface 定义的叫接口,它定义的是对结构的描述。下面的 info 使用 ES6 的新关键字 const 定义,通过 info: Info 指定 info 要实现 Info 这个结构,那 info 必须要包含 name 和 age 这两个字段。实际代码中却只有 name 字段,所以你可以看到 info 下面被红色波浪线标记了,说明它有问题。当你把鼠标放在 info 上时,VSCode 编辑器会做出如下提示:
如果上面这个小例子中你有很多概念都不了解,没关系,Lison 在后面的章节都会讲到。
配合VSCode这类编辑器,你可以借助编辑器的提示愉快地使用 TypeScript。另外值得一提的是,深受前端开发者喜爱的 VSCode 也是使用 TypeScript 开发的哦。
很多后端开发者,在做了很久的后端开发,习惯了 C++、Java 这些语言后,可能对我会有很多误解。就拿一个函数重载来说吧,在别的这些语言里,你可以定义多个同名函数,然后不同点在于参数个数、参数类型和函数体等,你可以给同一个函数传入不同参数,编译器就会知道你要调用的是哪个函数体;而我,也是有函数重载的概念的,只不过,我的重载是为了帮助编译器知道,你给同一个函数传入不同参数,返回值是什么情况;在 JavaScript 中,我们如果有一个函数,要根据传入参数不同,执行不同的逻辑,是需要自己在函数体内自行判断的。比如下面这个JavaScript 书写的例子:
-
const getResult =
input => {
-
if (typeof
input ===
'string')
return
input.length
-
else
if (typeof
input ===
'number')
return
input.toString()
-
else
return
input
-
}
-
这个例子很简单。如果输入的值是字符串类型,返回这个字符串的长度;如果是数值类型,返回这个数值变成字符串的结果;如果都不是,原样返回。可以看到,输入不同类型的值,返回的结果类型是不一样的。所以如果你要使用这个函数的返回值,就可能一不小心用错,比如输入123
,应该返回字符串 ‘123’
。如果你在结果上调用 toFixed 方法,如 getResult(123).toFixed()
,运行后就会报错,因为字符串是没有这个方法的。如果你使用我来书写,结果就不同了,我会在你写代码的时候就告诉你。来看怎么使用我来书写上面的例子:
-
function getResult (input: string): number
-
function getResult (input: number):
string
-
function getResult <T>(input: T): T
-
function getResult (input: any): any {
-
if (typeof
input ===
'string')
return
input.length
-
else
if (typeof
input ===
'number')
return
input.toString()
-
else
return
input
-
}
-
前三行组成了函数重载,第四行开始是实际函数体,之后你再调用 getResult
来看下效果:
这里输入123结果应该是字符串’123’,结果访问 toFixed 方法,字符串是没有这个方法的。
这里输入字符串’abc’,返回应该是他的长度数值3,结果访问它的length属性,数值是没有length属性的。
这里你传入一个对象,既不是字符串也不是数值,所以原样返回这个对象,编译器就知道你的res是对象 { a: ‘a’, b: ‘b’ }
啦。所以当你输入res然后输入 .
后,VSCode 就会给你列出有哪些属性可以访问。
是不是和你理解的函数重载有点不一样?所以一定要注意哦,不要用学习其他语言的思维来认识我哦,否则你会钻牛角尖的。上面例子的语法你可以不用在意,因为 Lison 都会详详细细地给你讲哒。
对了,另外还有一个我的好搭档,TSLint,也是追求极致的你不可或缺的。它和 ESLint 相似,都是能够对你的代码起到约束和提示作用,特别是团队协作的项目中,使用它可以让你们多个开发者都能够遵循相同的代码规范——大到各种语法,小到标点空格。搭配使用 VSCode 和 TSLint,再加上我强大的类型系统,写代码简直不要太嗨~
好了,向你介绍得差不多了,相信你对我已经有了一个大致的了解。下面让 Lison 向你客观地介绍下,我的发展趋势以及你为什么要与我为伴。
相信你在听完 TypeScript 的自我介绍之后,它的亮点你已经了解一二了。或许你还有些顾虑,毕竟学习 TypeScript 是需要时间的,你可能会担心 TypeScript 像 CoffeeScript 一样,随着 ES标准 的不断更新,渐渐退出大家的视线。下面让我们来看下 TypeScript 的发展趋势,来打消你的顾虑,同时让你对它有进一步的了解。
我们都知道 TypeScript 最主要的亮点是它的类型系统,这使得在编写代码的时候就能够检测到一些错误。而 JavaScript 是一门动态脚本语言,它不需要编译成二进制代码运行。Node 服务端代码也不需编译即可在服务器起一个服务,你甚至可以直接在服务器修改你的服务代码然后重启就可以,不需要编译等操作。这一切特点使得 JavaScript 的所有调试都需要在运行时才能进行,在编写代码的时候很多问题是无法提前知晓的,而且就JavaScript目前的使用场景来看,它在至少很长一段时间内会保持这样的特点。
而 TypeScript 和 JavaScript 不同的就是,它可以在你编写代码的时候,就对一些错误进行提示,还能在你使用某个数据的时候,为你列出这个数据可以访问的属性和方法。在 TypeScript 的自我介绍中我们已经看过几个简单的例子,想必你也知道它实现这些的效果了。当我们的项目较为庞大,需要由多人合作开发时,多人协作是需要沟通成本和 review 成本的。一些接口的定义,一些方法的使用,都可能因为个人习惯或沟通不畅导致逻辑实现的差异。而如果引入TypeScript,则会对一些实现进行强校验。如果不按接口实现,编译就没法通过,如果对代码质量要求较高,可以将严格检查全部打开,效果更好。
那么哪些项目适合用 TypeScript 开发呢,我总结了几类:
-
需要多人合作开发的项目
-
开源项目,尤其是工具函数或组件库
-
对代码质量有很高要求的项目
来看几个广为人知的使用 TypeScript 开发的经典项目:
-
VSCode:开源的高质量代码编辑器VSCode使用TypeScript开发,所以它天生就支持 TypeScript;
-
Angular & React & Vue3.0:现在三足鼎立的三个前端框架,Angular 和 React 已经使用 TypeScript编写,而在我编写专栏的同时,Vue3.0 将使用 TypeScript 进行重构,届时三个前端框架都使用TypeScript编写,如果使用TypeScript开发将会得到很好的类型支持。也可以看出,TypeScript 已经被广为接受。当然了,你依然可以使用JavaScript来开发前端项目,但是相信随着 Vue3.0 发布,TypeScript将会被越来越多的开发者所接受;
-
Ant Design:使用 React 开发项目的开发者大多应该都知道蚂蚁金服开源UI组件库Ant Design,同样使用TypeScript进行编写。保证了代码质量的同时,也能很好地支持开发者使用TypeScript进行React项目的开发。如果你使用 Vue 进行开发,Ant Design 也提供了Vue 版的组件库,风格和功能和 React 版的保持一致,共享单元测试和设计资源,对TypeScript的支持也一样很好。
TypeScript 在实现新特性的同时,时刻保持对ES标准的对齐。一些ECMAScript标准没有确定的内容,在 TypeScript 中已经率先支持了。所以在语法标准方面,可以说TypeScript是略微领先的,比如类的私有属性和方法。ES6标准对类的相关概念的定义中,并没有私有属性的概念,如果想实现私有属性,需要使用一些方法hack(可以参考阮一峰的《ECMAScript 6 入门》- 私有方法和私有属性);但是TypeScript是支持私有属性的,可以直接使用 private
指定一个私有属性。虽然ECMAScript新的提案提供了定义私有属性的方式,就是使用 #
来指定一个属性是私有的,但是到目前为止现在还没有编译器支持这种语法。
以上便是对 TypeScript 的大致介绍,接下来我们来看下本小册有哪些内容。
本小册共7大章节,7个章节的内容主要为:
-
入门准备:讲解学习 TypeScript 和使用 TypeScript 进行开发的一些方法和技巧,是授你以鱼之前的授你以渔,虽然后面章节会学习 TypeScript 的所有语法,但是掌握自学TypeScript的方法技巧,可以帮助你更好更快地学习 TypeScript,也方便你遇到问题时能够快速找到解决方案。
-
基础部分:这一章都是一些较为基础的知识,只要你有JavaScript的基础就能上手,学习起来不会有太大压力;学习完本章后,你就可以自己使用 TypeScript 写一些基本的日常开发中使用的逻辑了。
-
进阶部分:这一章你要做好心理准备了。作为进阶知识,不仅内容多一些,而且有些知识较为抽象,不好理解,需要你紧跟着 Lison 多练习多思考。这一章的知识有一些在平常的业务开发中很少用到,但是你也不可以掉以轻心,以免以后需要用到了,都不知道还有这高级内容。
-
知识整合:这一章是对前面学习的基础和进阶部分的知识的整合。学习这一章,需要前面章节的知识作为铺点,所以一定要把前面章节的知识掌握好哦。
-
项目配置及书写声明文件:这一章会详细讲解项目的配置项,也就是对 tsconfig.json 里的配置逐条讲解它的作用。之所以放到后面讲,是因为我们前面学习不需要用到这么多配置,但是学习所有配置,可以帮助你在开发自己项目时满足自己的开发需求。书写声明文件需要用到前面的语法知识,学会书写声明文件,我们就可以在使用了一些冷门的没有声明文件的JS库时,自行为它们书写声明文件,以便我们开发使用。
-
项目实战:这一章是实战部分,通过使用 TypeScript+Vue 开发一个简单后台。我会带着你从零创建一个项目,并实现目录中列出的功能,帮助你将学到的知识在实际开发中进行运用。即是对前面知识的复习,也是对理论知识到实践的转化,做完这个项目,相信你会对TypeScript项目开发有一个新的认识,再去独立开发项目,会轻松很多。
-
写在最后:这一章是一个总结。相信学到这一章的时候,你已经对 TypeScript 有了整体认知了。我将会在本章分享一些我的开发经验,帮助你在项目开发中少走弯路。
好了,在听完 TypeScript 的自我介绍和发展趋势的了解之后,让我们一起愉快地进入TypeScript 的学习中去吧。
02 TypeScript应该怎么学
02 TypeScript应该怎么学
更新时间:2019-11-26 09:50:53
理想必须要人们去实现它,它不但需要决心和勇敢而且需要知识。——吴玉章
如果你看过了本专栏的大纲,那你应该会有一种,哇,官方文档里列出的知识基本都讲了,这个专栏太细了的感觉。这一个小节我会教给大家如何去自学TypeScript。虽然你在学习本专栏的时候,Lison会手把手的带着你学习TypeScript的语法和实战。但我还是想给你讲讲如何自学TypeScript,在授你以鱼之前也会授你以渔的,这样TypeScript即使更新了,你也能毫无压力地迎接它的新特性。好,接下来让我们开始吧。
1.2.1 学会看文档
英文官方文档始终是及时更新的。但即便是官方的文档,有一些更新在更新日志里写了,而新手指南里却没有及时同步更新,所以有时看指南也会遇到困惑,就是文档里写的和你实际验证的效果不一样。遇到这种问题,首先确定你使用的TypeScript版本,然后去更新日志里根据不同版本找对这部分知识的更新记录。如果找到了,看下这是在哪个版本做的升级;如果你不放心,可以把TypeScript版本降到这个版本之前的一个版本,再验证一下。
TypeScript 是有一个中文文档的,但是这个文档只是对英文文档的翻译。官方文档中的小疏漏,这个文档也没有做校验,而且更新是有点滞后的。在写本专栏的时候,TypeScript最新发布的版本为3.4,但是中文文档还是在3.1。所以想了解TypeScript的最新动态,还是要看英文官方文档的。不过我们还是要感谢提供中文文档的译者,这对于英文不是很好的开发者帮助还是很大的。
1.2.2 学会看报错
我们在前面的例子中展示了 TypeScript 在编写代码的时候如何对错误进行提示。后面我们讲到项目搭建的时候,会使用 TSLint 对代码风格进行规范校验,根据 TSLint 配置不同,提示效果也不同。如果我们配置当书写的代码不符合规范,使用 error 级别来提示时,会和 TypeScript 编译报错一样,在问题代码下面用红色波浪线标出,鼠标放上去会有错误提示。所有如果我们使用了TSLint,遇到报错的时候,首先要区分是 TSLint 报错还是 TS 报错,来看下如何区分:
上面这个报错可以从红色方框中看到,标识了 tslint
,说明它是TSLint的报错。后面括号里标的是导致这条报错的规则名,规则可以在 tslint.json
文件里配置。关于 TSLint的使用,我们会在搭建开发环境一节讲解。示例中这条报错是因为 no-console
这个规则,也就是要求代码中不能有 console
语句,但是我们在开发时使用 console 来进行调试是很常见的,所以你可以通过配置 TSLint 关闭这条规则,这样就不会报错了。但我们应该遵守规范,当我们决定引入 TSLint 的时候,就说明这个项目对代码质量有更高的要求,我们不应该在书写代码遇到TSLint报错就修改规则,而是应该根据规则去修改代码。
上面这个报错可以从红色方框中看到,标识了 ts
,说明它是 TypeScript 编译器报的错误。在我们书写代码的时候,通过强类型系统,编译器可以在这个阶段就检测到我们的一些错误。后面括号里跟着的 2322
是错误代码,所有的错误代码你可以在文档的错误信息列表中查看。不过你一般并不需要去看文档,因为这里都会给你标出这个错误码对应的错误提示,而且这个错误信息根据你的编辑器语言可以提示中文错误信息。很明显这个错误是因为我们给 name 指定了类型为 string字符串 类型,而赋给它的值是123数值类型。
上面两种是在编写代码的时候就会遇到的错误提示。还有一种就是和 JavaScript 一样的,在运行时的报错,这种错误需要在浏览器控制台查看。如果你调试的是 node 服务端项目,那你要在终端查看。来看这个例子:
当我在代码中打印一个没有定义的变量时,在书写代码的时候会做提示,且当程序运行起来时,在浏览器控制台也可以看到报错。你可以打开浏览器的开发者工具(Windows系统按F12,Mac系统按control+option+i),在 Console 栏看到错误提示:
红色语句即错误信息。下面红色at
后面有个文件路径main.ts
,蓝色框中圈出的也是个文件路径,表示这个错误出现在哪个文件。这里是出现在main.ts
中,问号后面的cd49:12
表示错误代码在12行,点击这个路径即可跳到一个该文件的浏览窗口:
在这里我们就能直接看到我们的错误代码被红色波浪线标记了,这样你修改起错误来就很明确知道是哪里出错了。
1.2.3 学会看声明文件
声明文件我们会在后面讲。我们知道原来没有 TypeScript 的时候,有很多的 JS 插件和 JS 库,如果使用 TypeScript 进行开发再使用这些 JS 编写的插件和库,就得不到类型提示等特性的支持了,所以 TypeScript 支持为 JS 库添加声明文件,以此来提供声明信息。我们使用 TypeScript 编写的库和插件编译后也是 JS 文件,所以在编译的时候可以选择生成声明文件,这样再发布,使用者就依然能得到 TypeScript 特性支持。一些 JS 库的作者已经使用 TypeScript 进行了重写,有些则是提供了声明文件,一些作者没有提供声明文件的,大部分库都有社区的人为他们补充了声明文件,如果使用了自身没有提供声明文件的库时,可以使用npm install @types/{模块名}
来安装,或者运用我们后面讲到的知识自行为他们补充。
看这些库的声明文件能够帮你提高对 TypeScript 的了解程度。因为可能你在实际开发中所接触的场景不是很复杂,运用到的 TypeScript 语法点也不是很全面,所以就会导致经常用的你很熟悉,不经常用的慢慢就忘掉了,甚至有的自始至终你都没有使用过。很多知识你只看理论知识,或者看简单的例子,是没法真正理解并深刻记忆的,只有在实际场景中去使用一下,才能加深理解。所以我们可以从这些库的声明文件入手,还有就是从 TypeScript 内置的 lib 声明文件入手。
安装好 TypeScript 后,我们可以在 node_modules 文件夹下找到 typescript 文件夹,里面有个 lib 文件夹,lib 文件夹根目录下有很多以 lib.
开头的 .d.ts
文件。这些文件,就是我们在开发时如果需要用到相关内容,需要在 tsconfig.json
文件里配置引入的库的声明文件,这个配置我们后面会讲到。先简单举个例子,比如我们要使用 DOM 操作相关的语法,比如我们获取了一个 button 按钮的节点,那么我们就可以指定它的类型为 HTMLButtonElement,那么我们再访问这个节点的属性的时候,编辑器就会给你列出 button 节点拥有的所有属性方法了;如果我们要用到这个类型接口,那我们就需要引入 lib.dom.d.ts
也就是dom这个 lib。这里如果你对一些提到的概念不明白,你可以先忽略,因为后面都会讲到。这里我要告诉你的就是,你应该学着看这些声明文件,看看它们对于一些内容的声明是如何定义的,能够帮你见识到各种语法的运用。
1.2.4 学会搜问题和提问
实际开发中,有时候你难免会遇到一些文档里没有提到的各种各样的奇怪问题。解决问题的途径有很多,请教有经验的人是最简单的啦,但前提是你身边有个随叫随到的大神,可这样的人一般很少有,所以还是看看我推荐给你的解决问题的途径吧:
途径1:百度 or Google
一般来说大众的问题都能在百度找到,但是开发问题 Google 能够帮你找到一些高质量的国外答疑帖,所以这两个搜索引擎你都可以试试,这个途径是你遇到问题之后的首选。
途径2:看issue
TypeScript 的问答确实要比很多框架或者基础知识的少很多。如果搜索引擎找不到,你可以到 github 上 TypeScript 的官方仓库,在issues里可以通过问题关键字搜索,看看有没有人反馈过这个问题。这里要注意,搜索的是关键字,而不是把你的报错信息完整输进去,这样基本很难搜到。你应该挑选出错误信息中比较具有代表性的单词进行搜索,因为这和搜索引擎不一样,issues 提供的搜索还不是很强大。
途径3:去提问
如果上面两个途径都没找到,你只能自行提问了,这也是一个造福后人的方法。比较受欢迎的提问网站:国内你可以试试 segmentFault,国外可以试试stackOverflow,还有就是 TypeScript的issues 了。但要注意如果在 stackOverflow 和 issues 中提问,最好最好用英文。
1.2.5 看优秀项目源码
这个学习方法是比较高阶的了,看一些优秀的开源项目源码可以为你提供思路。你还可以借鉴到同一个逻辑不同人的实现方式。源码去哪里找呢,当然首选是 Github 了,进入 Github 后,你可以在顶部的搜索栏搜索你想要找的项目关键字,比如你想找个Todo应用的项目源码,那就搜”todo”。然后在语言栏选择 TypeScript,这样就会筛选出使用 TypeScript 编写的项目:
最后选择 star 较多的项目,说明这个项目受到了更多人的认可:
好了,以上就是自学 TypeScript 的一些方法途径。当然了,我们的 TypeScript 知识还是会由我带着大家一起学习哒,所以只要跟紧了别掉队,不怕你学不会。
03 VSCode揭秘和搭建开发环境
03 VSCode揭秘和搭建开发环境
更新时间:2019-11-26 09:50:59
宝剑锋从磨砺出,梅花香自苦寒来。——佚名
这节课我们要做的就是在砍柴之前先磨刀,学习如何借助VSCode愉快高效地开发TypeScript项目,我们来一步一步让VSCode对TypeScript的支持更强大。如果你已经习惯了使用别的编辑器,那你也可以自行搜索下,本节课提到的内容在你使用的编辑器是否有对应的替代品。
1.3.1 安装和基本配置
如果你还没有使用过VSCode,当然先要去官网下载了,下载安装我就不多说了,安装好之后,我们先来配置几个基本的插件。
(1)汉化
如果你英语不是很好,配置中文版界面是很有必要的,安装个插件就可以了。打开VSCode之后在编辑器左侧找到这个拓展按钮,点击,然后在搜索框内搜索关键字”Chinese”,这里图中第一个插件就是。直接点击install安装,安装完成后重启VSCode即可。
(2)编辑器配置
有一些编辑器相关配置,需要在项目根目录下创建一个.vscode
文件夹,然后在这个文件夹创建一个settings.json
文件,编辑器的配置都放在这里,并且你还需要安装一个插件EditorConfig for VS Code这样配置才会生效。配置文件里我们来看几个简单而且使用的配置:
-
{
-
"tslint.configFile":
"./tslint.json",
-
"tslint.autoFixOnSave":
true,
-
"editor.formatOnSave":
true
-
}
-
tslint.configFile
用来指定tslint.json文件的路径,注意这里是相对根目录的;
tslint.autoFixOnSave
设置为true则每次保存的时候编辑器会自动根据我们的tslint配置对不符合规范的代码进行自动修改;
tslint.formatOnSave
设为true则编辑器会对格式在保存的时候进行整理。
(3)TypeScript相关插件
TSLint(deprecated)是一个通过tslint.json配置在你写TypeScript代码时,对你的代码风格进行检查和提示的插件。关于TSLint的配置,我们会在后面讲解如何配置,它的错误提示效果在我们之前的例子已经展示过了。
TSLint Vue加强了对Vue中的TypeScript语法语句进行检查的能力。如果你使用TypeScript开发Vue项目,而且要使用TSLint对代码质量进行把控,那你应该需要这个插件。
(4)框架相关
如果你使用Vue进行项目开发,那Vue相关的插件也是需要的,比如Vue 2 Snippets。
Vetur插件是Vue的开发辅助工具,安装它之后会得到代码高亮、输入辅助等功能。
(5)提升开发体验
Auto Close Tag插件会自动帮你补充HTML闭合标签,比如你输完<button>
的后面的尖括号后,插件会自动帮你补充</button>
;
Auto Rename Tag插件会在你修改HTML标签名的时候,自动帮你把它对应的闭标签同时修改掉;
Bracket Pair Colorizer插件会将你的括号一对一对地用颜色进行区分,这样你就不会被多层嵌套的括号搞晕了,来看看它的样子:
Guides插件能够帮你在代码缩进的地方用竖线展示出索引对应的位置,而且点击代码,它还会将统一代码块范围的代码用统一颜色竖线标出,如图:
1.3.2 常用功能
(1)终端
在VSCode中有终端窗口,点击菜单栏的【查看】-【终端】,也可以使用快捷键 ”control+`“ 打开。这样可以直接在编辑器运行启动命令,启动项目,边写代码边看报错。
(2)用户代码片段
一些经常用到的重复的代码片段,可以使用用户代码片段
配置,这样每次要输入这段代码就不用一行一行敲了,直接输入几个标示性字符即可。在VSCode左下角有个设置按钮,点击之后选择【用户代码片段】,在弹出的下拉列表中可以选择【新建全局代码片段文件】,这样创建的代码片段是任何项目都可用的;可以选择【新建”项目名”文件夹的代码片段文件】,这样创建的代码片段只在当前项目可用。创建代码片段文件后它是一个类似于json的文件,文件有这样一个示例:
-
{
-
// Place your global snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
-
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
-
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
-
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
-
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
-
// Placeholders with the same ids are connected.
-
// Example:
-
// "Print to console": {
-
// "scope": "javascript,typescript",
-
// "prefix": "log",
-
// "body": [
-
// "console.log('$1');",
-
// "$2"
-
// ],
-
// "description": "Log output to console"
-
// }
-
}
-
我们来看一下其中的几个关键参数:
-
Print to console
是要显示的提示文字 -
scope
是代码片段作用的语言类型 -
prefix
是你输入这个名字之后,就会出现这个代码片段的选项回车即可选中插入 -
body
就是你的代码片段实体 -
$1
是输入这个片段后光标放置的位置,这个$1不是内容,而是一个占位 -
description
是描述。如果你要输入的就是字符串lt;$
好了,暂时VSCode的相关介绍就是这么多,剩下的一些配置tslint等工作,我们会在搭建开发环境和后面的开发中讲到。
1.3.3 搭建开发环境
接下来我们开始从零搭建一个开发环境,也就是一个基础前端项目。后面课程中讲到的语法知识,你都可以在这个项目中去尝试,接下来我们就一步一步来搭建我们的开发环境啦。
在开始之前,你要确定你的电脑有node的环境,如果你没有安装过node,先去Node.js下载地址下载对应你系统的node.js安装包,下载下来进行安装。我在专栏中使用的是v10.15.3版本,你可以尝试最新稳定版本。如果发现启动项目遇到问题,可能是一些安装的依赖不兼容新版本,那你可以安装和我一样的版本。
node安装好之后,可以在命令行运行node -v
来查看node的版本号。如果正确打印出版本号说明安装成功。npm是node自带的包管理工具,会在安装node的时候自动进行安装,可以使用npm -v
来查看npm的版本,检验是否安装成功。我们会使用npm来安装我们所需要的模块和依赖,如果你想全局安装一个tslint模块,可以这样进行安装:
-
npm
install -g tslint
-
如果这个模块要作为项目依赖安装,去掉-g参数即可。更多关于node的知识,你可以参考node官方文档或node中文文档,更多关于npm的使用方法,可以参考npm官方文档或npm中文文档。
(1)初始化项目
新建一个文件夹“client-side”,作为项目根目录,进入这个文件夹:
-
mkdir
client-side
-
cd
client-side
-
我们先使用 npm 初始化这个项目:
-
# 使用npm默认package.json配置
-
npm
init
-y
-
# 或者使用交互式自行配置,遇到选项如果直接敲回车即使用括号内的值
-
npm
init
-
package name:
(client-side)
# 可敲回车即使用client-side这个名字,也可输入其他项目名
-
version:
(1.0.0)
# 版本号,默认1.0.0
-
description:
# 项目描述,默认为空
-
entry point:
(index.js)
# 入口文件,我们这里改为./src/index.ts
-
test command:
# 测试指令,默认为空
-
git repository:
# git仓库地址,默认为空
-
keywords:
# 项目关键词,多个关键词用逗号隔开,我们这里写typescript,client,lison
-
author:
# 项目作者,这里写lison<lison16new@163.com>
-
license:
(ISC)
# 项目使用的协议,默认是ISC,我这里使用MIT协议
-
# 最后会列出所有配置的项以及值,如果没问题,敲回车即可。
-
这时我们看到了在根目录下已经创建了一个 package.json 文件,接下来我们创建几个文件夹:
-
src:用来存放项目的开发资源,在 src 下创建如下文件夹:
-
utils:和业务相关的可复用方法
-
tools:和业务无关的纯工具函数
-
assets:图片字体等静态资源
-
api:可复用的接口请求方法
-
config:配置文件
-
-
typings:模块声明文件
-
build:webpack 构建配置
接下来我们在全局安装typescript
,全局安装后,你就可以在任意文件夹使用tsc命令:
-
npm
install typescript -g
-
如果全局安装失败,多数都是权限问题,要以管理员权限运行。
安装成功后我们进入项目根目录,使用typescript进行初始化:
-
tsc
--init
-
注意:运行的指令是tsc,不是typescript。
这时你会发现在项目根目录多了一个 tsconfig.json 文件,里面有很多内容。而且你可能会奇怪,json 文件里怎么可以使用//
和/**/
注释,这个是 TS 在 1.8 版本支持的,我们后面课程讲重要更新的时候会讲到。
tsconfig.json 里默认有 4 项没有注释的配置,有一个需要提前讲下,就是”lib”这个配置项,他是一个数组,他用来配置需要引入的声明库文件,我们后面会用到ES6语法,和DOM相关内容,所以我们需要引入两个声明库文件,需要在这个数组中添加”es6″和”dom”,也就是修改数组为[“dom”, “es6”]
,其他暂时不用修改,接着往下进行。
然后我们还需要在项目里安装一下typescript,因为我们要搭配使用webpack进行编译和本地开发,不是使用tsc指令,所以要在项目安装一下:
npm install typescript
(2)配置TSLint
接下来我们接入TSLint,如果你对代码的风格统一有要求,就需要用到TSLint了,另外TSLint会给你在很多地方起到提示作用,所以还是建议加入的。接下来我们来接入它。
首先需要在全局安装TSLint,记着要用管理员身份运行:
npm install tslint -g
然后在我们的项目根目录下,使用TSLint初始化我们的配置文件:
tslint -i
运行结束之后,你会发现项目根目录下多了一个tslint.json
文件,这个就是TSLint的配置文件了,它会根据这个文件对我们的代码进行检查,生成的tslint.json文件有下面几个字段:
-
{
-
"defaultSeverity":
"error",
-
"extends": [
-
"tslint:recommended"
-
],
-
"jsRules": {},
-
"rules": {},
-
"rulesDirectory": []
-
}
-
defaultSeverity是提醒级别,如果为error则会报错,如果为warning则会警告,如果设为off则关闭,那TSLint就关闭了;
-
extends可指定继承指定的预设配置规则;
-
jsRules用来配置对
.js
和.jsx
文件的校验,配置规则的方法和下面的rules一样; -
rules是重点了,我们要让TSLint根据怎样的规则来检查代码,都是在这个里面配置,比如当我们不允许代码中使用
eval
方法时,就要在这里配置"no-eval": true
; -
rulesDirectory可以指定规则配置文件,这里指定相对路径。
以上就是我们初始化的时候TSLint生成的tslint.json文件初始字段,如果你发现你生成的文件和这里看到的不一样,可能是TSLint版本升级导致的,你可以参照TSLint配置说明了解他们的用途。如果你想要查看某条规则的配置及详情,可以参照TSLint规则说明。
(3)配置webpack
接下来我们要搭配使用 webpack 进行项目的开发和打包,先来安装 webpack、webpack-cli 和 webpack-dev-server:
npm install webpack webpack-cli webpack-dev-server -D
我们将它们安装在项目中,并且作为开发依赖(-D)安装。接下来添加一个 webpack 配置文件,放在 build 文件夹下,我们给这个文件起名 webpack.config.js,然后在 package.json 里指定启动命令:
-
{
-
"scripts": {
-
"start":
"cross-env NODE_ENV=development webpack-dev-server --mode=development --config build/webpack.config.js"
-
}
-
}
这里我们用到一个插件”cross-env”,并且后面跟着一个参数 NODEENV=development,这个用来在 webpack.config.js 里通过 process.env.NODEENV 来获取当前是开发还是生产环境,这个插件要安装:
npm install cross-env
紧接着我们要在 webpack.config.js 中书写配置:
-
const HtmlWebpackPlugin =
require(
"html-webpack-plugin");
-
const { CleanWebpackPlugin } =
require(
"clean-webpack-plugin");
-
-
module.
exports = {
-
// 指定入口文件
-
// 这里我们在src文件夹下创建一个index.ts
-
entry:
"./src/index.ts",
-
// 指定输出文件名
-
output: {
-
filename:
"main.js"
-
},
-
resolve: {
-
// 自动解析一下拓展,当我们要引入src/index.ts的时候,只需要写src/index即可
-
// 后面我们讲TS模块解析的时候,写src也可以
-
extensions: [
".tsx",
".ts",
".js"]
-
},
-
module: {
-
// 配置以.ts/.tsx结尾的文件都用ts-loader解析
-
// 这里我们用到ts-loader,所以要安装一下
-
// npm install ts-loader -D
-
rules: [
-
{
-
test:
/\.tsx?$/,
-
use:
"ts-loader",
-
exclude:
/node
modules/
-
}
-
]
-
},
-
// 指定编译后是否生成source-map,这里判断如果是生产打包环境则不生产source-map
-
devtool: process.env.NODEENV ===
"production" ?
false :
"inline-source-map",
-
// 这里使用webpack-dev-server,进行本地开发调试
-
devServer: {
-
contentBase:
"./dist",
-
stats:
"errors-only",
-
compress:
false,
-
host:
"localhost",
-
port:
8089
-
},
-
// 这里用到两个插件,所以首先我们要记着安装
-
// npm install html-webpack-plugin clean-webpack-plugin -D
-
plugins: [
-
// 这里在编译之前先删除dist文件夹
-
new CleanWebpackPlugin({
-
cleanOnceBeforeBuildPatterns: [
"./dist"]
-
}),
-
// 这里我们指定编译需要用模板,模板文件是./src/template/index.html,所以接下来我们要创建一个index.html文件
-
new HtmlWebpackPlugin({
-
template:
"./src/template/index.html"
-
})
-
]
-
};
-
这里我们用到了两个webpack插件,第一个clean-webpack-plugin
插件用于删除某个文件夹,我们编译项目的时候需要重新清掉上次打包生成的dist文件夹,然后进行重新编译,所以需要用到这个插件将上次打包的dist文件夹清掉。
第二个html-webpack-plugin
插件用于指定编译的模板,这里我们指定模板为"./src/template/index.html"
文件,打包时会根据此html文件生成页面入口文件。
接下来我们创建这个 index.html 模板:
-
<!DOCTYPE html>
-
<html lang="en">
-
<head>
-
<meta charset="UTF-8" />
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
-
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
-
<title>TS-Learning
</title>
-
</head>
-
-
<body>
</body>
-
</html>
现在我们运行如下命令来启动本地服务:
npm run start
我们看到启动成功了,接下来我们在 index.ts 文件里写一点逻辑:
-
// index.ts
-
let a:
number =
123;
-
-
const h1 =
document.createElement(
"h1");
-
h1.innerHTML =
"Hello, I am Lison";
-
document.body.appendChild(h1);
-
当我们保存代码的时候,开发服务器重新编译了代码,并且我们的浏览器也更新了。
我们再来配置一下打包命令,在 package.json 的 scripts 里增加 build 指令:
-
{
-
"scripts": {
-
"test":
"echo \"Error: no test specified\" && exit 1",
-
"start":
"cross-env NODE
ENV=development webpack-dev-server --mode=development --config ./build/webpack.config.js",
-
"build": "cross-env NODE
ENV=production webpack --mode=production --config ./build/webpack.config.js"
-
}
-
}
-
同样通过cross-env NODE_ENV=production
传入参数。现在我们运行如下命令即可执行打包:
npm run build
现在我们前端项目的搭建就大功告成了,我们后面的课程都会在这个基础上进行示例的演示。大家最好都自己操作一遍,把开发环境的搭建流程走一下,如果中间遇到了报错仔细看一下报错信息。下节课开始我们就正式的步入TypeScript的学习中了,我们下节课见。
04 八个JS中你见过的类型
04 八个JS中你见过的类型
更新时间:2019-07-01 14:16:50
生活永远不像我们想像的那样好,但也不会像我们想像的那样糟。 ——莫泊桑
这小节你学习起来会很轻松,这是你正式接触 TypeScript 语法的第一节课,是最最基础的语法单元。这节课我们将学习在 JavaScript 中现有的八个数据类型,当然这并不是 JavaScript 中的所有数据类型,而是现在版本的 TypeScript 支持的基本类型,在学习基础类型之前,我们先来看下如何为一个变量指定类型:
为一个变量指定类型的语法是使用”变量: 类型”的形式,如下:
-
let num:
number =
123
-
如果你没有为这个变量指定类型,编译器会自动根据你赋给这个变量的值来推断这个变量的类型:
-
let num =
123
-
num =
'abc' //
error 不能将类型“
"123"”分配给类型“number”
-
当我们给num
赋值为123但没有指定类型时,编译器推断出了num
的类型为number
数值类型,所以当给num
再赋值为字符串”abc”时,就会报错。
这里还有一点要注意,就是number和Number的区别:TS中指定类型的时候要用number
,这个是TypeScript的类型关键字。而Number
为JavaScript的原生构造函数,用它来创建数值类型的值,它俩是不一样的。包括你后面见到的string、boolean等都是TypeScript的类型关键字,不是JavaScript语法,这点要区分开。接下来我们来看本节课的重点:八个JS中你见过的类型。
2.1.1 布尔类型
类型为布尔类型的变量的值只能是 true 或 false,如下:
-
let
bool:
boolean =
false;
-
bool =
true;
-
bool =
123;
// error 不能将类型"123"分配给类型"boolean"
-
当然了,赋给 bool 的值也可以是一个计算之后结果是布尔值的表达式,比如:
-
let
bool: boolean = !!
0
-
console.
log(
bool)
// false
-
2.1.2 数值类型
TypeScript 和 JavaScript 一样,所有数字都是浮点数,所以只有一个number
类型,而没有int
或者float
类型。而且 TypeScript 还支持 ES6 中新增的二进制和八进制数字字面量,所以 TypeScript 中共支持二、八、十和十六四种进制的数值。
-
let num: number;
-
num =
123;
-
num =
"123"; // error 不能将类型
"123"分配给类型
"number"
-
num =
0b
1111011; // 二进制的
123
-
num =
0o
173; // 八进制的
123
-
num =
0x
7b; // 十六进制的
123
-
2.1.3 字符串
字符串类型中你可以使用单引号和双引号包裹内容,但是可能你使用的 tslint 规则会对引号进行检测,使用单引号还是双引号可以在 tslint 规则里配置。你还可以使用 ES6 语法——模板字符串,拼接变量和字符串更为方便。
-
let
str: string =
"Lison";
-
str =
"Li";
-
const first =
"Lison";
-
const last =
"Li";
-
str =
${first} ${last}
;
-
console.log(
str)
// 打印结果为:Lison Li
-
另外还有个和字符串相关的类型:字符串字面量类型。即把一个字符串字面量作为一种类型,比如上面的字符串”Lison”,当你把一个变量指定为这个字符串类型的时候,就不能再赋值为其他字符串值了,如:
-
let str:
'Lison'
-
str =
'haha' //
error 不能将类型“
"haha"”分配给类型“
"Lison"”
-
2.1.4 数组
在 TypeScript 中有两种定义数组的方式:
-
let list1:
number[]
= [
1,
2,
3]
;
-
let list2:
Array<number>
= [
1,
2,
3]
;
-
第一种形式通过number[]
的形式来指定这个类型元素均为number类型的数组类型,这种写法是推荐的写法,当然你也可以使用第二种写法。注意,这两种写法中的number
指定的是数组元素的类型,你也可以在这里将数组的元素指定为任意类型。如果你要指定一个数组里的元素既可以是数值也可以是字符串,那么你可以使用这种方式:number|string[]
,这种方式我们在后面学习联合类型的时候会讲到。
当你使用第二种形式定义时,tslint 可能会警告让你使用第一种形式定义,如果你就是想用第二种形式,可以通过在 tslint.json 的 rules 中加入"array-type": [false]
关闭 tslint 对这条的检测。
后面我们讲接口的时候,还会讲到数组的一个特殊类型:ReadonlyArray,即只读数组。
2.1.5 null 和 undefined
null 和 undefined 有一些共同特点,所以我们放在一起讲。说它们有共同特点,是因为在 JavaScript 中,undefined 和 null 是两个基本数据类型。在 TypeScript 中,这两者都有各自的类型即 undefined 和 null,也就是说它们既是实际的值,也是类型,来看实际例子:
-
let u:
undefined =
undefined;
// 这里可能会报一个tslint的错误:Unnecessary initialization to
'undefined',就是不能给一个值赋
undefined,但我们知道这是可以的,所以如果你的代码规范想让这种代码合理化,可以配置tslint,将
"no-unnecessary-initializer"设为
false即可
-
let n:
null =
null;
-
默认情况下 undefined 和 null 可以赋值给任意类型的值,也就是说你可以把 undefined 赋值给 void 类型,也可以赋值给 number 类型。当你在 tsconfig.json 的”compilerOptions”里设置了"strictNullChecks": true
时,那必须严格对待。undefined 和 null 将只能赋值给它们自身和 void 类型,void类型我们后面会学习。
2.1.6 object
object 在 JS 中是引用类型,它和 JS 中的其他基本类型不一样,像 number、string、boolean、undefined、null 这些都是基本类型,这些类型的变量存的是他们的值,而 object 类型的变量存的是引用,看个简单的例子:
-
let strInit =
"abc";
-
let strClone = strInit;
-
strClone =
"efg";
-
console.log(strInit);
// 'abc'
-
-
let objInit = {
a:
"aa" };
-
let objClone = objInit;
-
console.log(objClone)
// {a:"aa"}
-
objInit.a =
"bb";
-
console.log(objClone);
// { a: 'bb' }
-
通过例子可以看出,我们修改 objInit 时,objClone 也被修改了,是因为 objClone 保存的是 objInit 的引用,实际上 objInit 和 objClone 是同一个对象。
当我们希望一个变量或者函数的参数的类型是一个对象的时候,使用这个类型,比如:
-
let
obj: object
-
obj = {
name:
'Lison' }
-
obj =
123
// error 不能将类型“123”分配给类型“object”
-
这里有一点要注意了,你可能会想到给 obj 指定类型为 object 对象类型,然后给它赋值一个对象,后面通过属性访问操作符访问这个对象的某个属性,实际操作一下你就会发现会报错:
-
let obj:
object
-
obj = { name:
'Lison' }
-
console.log(obj.name)
// error 类型“object”上不存在属性“name”
-
这里报错说类型 object 上没有 name 这个属性。如果你想要达到这种需求你应该使用我们后面章节要讲到的接口,那 object 类型适合什么时候使用呢?我们前面说了,当你希望一个值必须是对象而不是数值等类型时,比如我们定义一个函数,参数必须是对象,这个时候就用到object类型了:
-
function getKeys (obj: object) {
-
return
Object.keys(obj)
// 会以列表的形式返回obj中的值
-
}
-
getKeys({ a:
'a' })
// ['a']
-
getKeys(
123)
// error 类型“123”的参数不能赋给类型“object”的参数
-
这里涉及到的函数的相关知识,我们会在后面章节介绍的,你只要在这里明白object类型的使用就可以了。
2.1.6 symbol
Symbol 是 ES6 加入的新的基础数据类型,因为它的知识比较多,所以我们单独在后面的一节进行讲解。
本节小结
本小节我们学习了八个在JavaScript中我们就见过的数据类型,它们是:布尔类型、数值类型、字符串、数组、null、undefined、object以及ES6中新增的symbol。在TypeScript中它们都有对应的类型关键字,对应关系为:
-
布尔类型:boolean
-
数值类型:number
-
字符串类型:string
-
数组:Array<type>或type[]
-
对象类型:object
-
Symbol类型:symbol
-
null和undefined:null 和 undefined,这个比较特殊,它们自身即是类型
这些类型是基础,我们后面的高级类型很多都是它们的组合或者变形,所以一定要把这些基础先学会。下个小节我们将学习 TypeScript 中新增的几种类型,了解更多基本类型。
05 TS中补充的六个类型
05 TS中补充的六个类型
更新时间:2019-07-15 18:58:20
衡量一个人的真正品格,是看他在知道没人看见的时候干些什么。 ——孟德斯鸠
上个小节我们学习了八个JavaScript中常见的数据类型,你也学会了如何给一个变量指定类型。本小节我们将接触几个TypeScript中引入的新类型,这里面可能有你在其他强类型语言中见过的概念,接下来让我们一起来学习。
2.2.1 元组
元组可以看做是数组的拓展,它表示已知元素数量和类型的数组。确切地说,是已知数组中每一个位置上的元素的类型,来看例子:
-
let tuple: [
string, number, boolean];
-
tuple = [
"a",
2,
false];
-
tuple = [
2,
"a",
false]; //
error 不能将类型“number”分配给类型“
string”。 不能将类型“
string”分配给类型“number”。
-
tuple = [
"a",
2]; //
error Property
'2' is missing
in
type
'[string, number]' but required
in
type
'[string, number, boolean]'
-
可以看到,上面我们定义了一个元组 tuple,它包含三个元素,且每个元素的类型是固定的。当我们为 tuple 赋值时:各个位置上的元素类型都要对应,元素个数也要一致。
我们还可以给单个元素赋值:
-
tuple[
1] =
3;
-
这里我们给元组 tuple 的索引为 1 即第二个元素赋值为 3,第二个元素类型为 number,我们赋值给 3,所以没有问题。
当我们访问元组中元素时,TypeScript 会对我们在元素上做的操作进行检查:
-
tuple
[0]
.split(
":");
// right 类型"string"拥有属性"split"
-
tuple
[1]
.split(
":");
// error 类型“number”上不存在属性“split”
-
上面的例子中,我们访问的 tuple 的第二个元素的元素类型为 number,而数值没有 split 方法,所以会报错。
在 2.6 版本之前,TypeScript 对于元组长度的校验和 2.6 之后的版本有所不同,我们来看下面的例子,前后版本对于该情况的处理:
-
let tuple: [
string,
number];
-
tuple = [
"a",
2];
// right 类型和个数都对应,没问题
-
// 2.6版本之前如下也不会报错
-
tuple = [
"a",
2,
"b"];
-
// 2.6版本之后如下会报错
-
tuple = [
"a",
2,
"b"];
// error 不能将类型“[string, number, string]”分配给类型“[string, number]”。 属性“length”的类型不兼容。
-
这个赋给元组的值有三个元素,是比我们定义的元组类型元素个数多的:
-
在 2.6 及之前版本中,超出规定个数的元素称作越界元素,但是只要越界元素的类型是定义的类型中的一种即可。比如我们定义的类型有两种:string 和 number,越界的元素是 string 类型,属于联合类型
string | number
,所以没问题,联合类型的概念我们后面会讲到。 -
在 2.6 之后的版本,去掉了这个越界元素是联合类型的子类型即可的条件,要求元组赋值必须类型和个数都对应。
在 2.6 之后的版本,[string, number]元组类型的声明效果上可以看做等同于下面的声明:
-
interface Tuple extends Array<number | string> {
-
0:
string;
-
1: number;
-
length:
2;
-
}
-
上面这个声明中,我们定义接口Tuple
,它继承数组类型,并且数组元素的类型是 number 和 string
构成的联合类型,这样接口Tuple
就拥有了数组类型所有的特性。并且我们明确指定索引为0的值为string
类型,索引为1的值为number
类型,同时我们指定 length
属性的类型字面量为 2,这样当我们再指定一个类型为这个接口Tuple
的时候,这个值必须是数组,而且如果元素个数超过2个时,它的length就不是2是大于2的数了,就不满足这个接口定义了,所以就会报错;当然,如果元素个数不够2个也会报错,因为索引为0或1的值缺失。接口我们后面会在后面专门的一节来讲,所以暂时不懂也没关系。
如果你想要和 2.6 及之前版本一样的元组特性,那你可以这样定义接口:
-
interface Tuple extends Array<number | string> {
-
0:
string;
-
1: number;
-
}
-
也就是去掉接口中定义的length: 2
,这样Tuple
接口的length
就是从Array
继承过来的number
类型,而不用必须是2
了。
2.2.2 枚举
enum
类型在 C++这些语言中比较常见,TypeScript 在 ES 原有类型基础上加入枚举类型,使我们在 TypeScript 中也可以给一组数值赋予名字,这样对开发者来说较为友好。比如我们要定义一组角色,每一个角色用一个数字代表,就可以使用枚举类型来定义:
-
enum Roles {
-
SUPER_ADMIN,
-
ADMIN,
-
USER
-
}
-
上面定义的枚举类型 Roles 里面有三个值,TypeScript 会为它们每个值分配编号,默认从 0 开始,依次排列,所以它们对应的值是:
-
enum Roles {
-
SUPER_ADMIN =
0,
-
ADMIN =
1,
-
USER =
2
-
}
-
当我们使用的时候,就可以使用名字而不需要记数字和名称的对照关系了:
-
const superAdmin = Roles.SUPER_ADMIN;
-
console.
log(superAdmin);
// 0
-
你也可以修改这个数值,比如你想让这个编码从 1 开始而不是 0,可以如下定义:
-
enum Roles {
-
SUPER_ADMIN =
1,
-
ADMIN,
-
USER
-
}
-
这样当你访问Roles.ADMIN
时,它的值就是 2 了。
你也可以为每个值都赋予不同的、不按顺序排列的值:
-
enum Roles {
-
SUPER_ADMIN =
1,
-
ADMIN =
3,
-
USER =
7
-
}
-
通过名字 Roles.SUPER_ADMIN 可以获取到它对应的值 1,同时你也可以通过值获取到它的名字,以上面任意数值这个例子为前提:
-
console.log(Roles[
3]);
//
'ADMIN'
-
更多枚举的知识我们会在后面专门的一节讲解,在这里我们只是先有个初步的认识即可。
2.2.3 Any
JavaScript 的类型是灵活的,程序有时也是多变的。有时,我们在编写代码的时候,并不能清楚地知道一个值到底是什么类型,这时就需要用到 any 类型,即任意类型。我们来看例子:
-
let
value: any;
-
value =
123;
-
value =
"abc";
-
value =
false;
-
你可以看到,我们定义变量 value,指定它的类型为 any,接下来赋予任何类型的值都是可以的。
我们还可以在定义数组类型时使用 any 来指定数组中的元素类型为任意类型:
-
const
array: any[] = [
1,
"a",
true];
-
但是请注意,不要滥用 any,如果任何值都指定为 any 类型,那么 TypeScript 将失去它的意义。
所以如果类型是未知的,更安全的做法是使用unknown类型,我们本小节后面会讲到。
2.2.4 void
void 和 any 相反,any 是表示任意类型,而 void 是表示没有任意类型,就是什么类型都不是,这在我们定义函数,函数没有返回值时会用到:
-
const consoleText = (text:
string):
void => {
-
console.log(text);
-
};
-
这个函数没有返回任何的值,所以它的返回类型为 void。现在你只需知道 void 表达的含义即可,后面我们会用专门的一节来学习函数。
void 类型的变量只能赋值为 undefined
和 null
,其他类型不能赋值给 void 类型的变量。
2.2.5 never
never 类型指那些永不存在的值的类型,它是那些总会抛出异常或根本不会有返回值的函数表达式的返回值类型,当变量被永不为真的类型保护(后面章节会详细介绍)所约束时,该变量也是 never 类型。
这个类型比较难理解,我们先来看几个例子:
-
const errorFunc = (message:
string):
never => {
-
throw
new
Error(message);
-
};
-
这个 errorFunc 函数总是会抛出异常,所以它的返回值类型是 never,用来表明它的返回值是永不存在的。
-
const infiniteFunc = ():
never => {
-
while (
true) {}
-
};
-
infiniteFunc
也是根本不会有返回值的函数,它和之前讲 void 类型时的consoleText
函数不同,consoleText
函数没有返回值,是我们在定义函数的时候没有给它返回值,而infiniteFunc
是死循环是根本不会返回值的,所以它们二者还是有区别的。
never 类型是任何类型的子类型,所以它可以赋值给任何类型;而没有类型是 never 的子类型,所以除了它自身没有任何类型可以赋值给 never 类型,any 类型也不能赋值给 never 类型。我们来看例子:
-
let neverVariable =
(() => {
-
while (
true) {}
-
})();
-
neverVariable =
123;
// error 不能将类型
"number"分配给类型
"never"
-
上面例子我们定义了一个立即执行函数,也就是"let neverVariable = "
右边的内容。右边的函数体内是一个死循环,所以这个函数调用后的返回值类型为 never,所以赋值之后 neverVariable 的类型是 never 类型,当我们给 neverVariable 赋值 123 时,就会报错,因为除它自身外任何类型都不能赋值给 never 类型。
2.2.6 unknown
unknown
类型是TypeScript在3.0版本新增的类型,它表示未知的类型,这样看来它貌似和any很像,但是还是有区别的,也就是所谓的“unknown相对于any是安全的”。怎么理解呢?我们知道当一个值我们不能确定它的类型的时候,可以指定它是any类型;但是当指定了any类型之后,这个值基本上是“废”了,你可以随意对它进行属性方法的访问,不管有的还是没有的,可以把它当做任意类型的值来使用,这往往会产生问题,如下:
-
let
value:
any
-
console
.log(
value
.name)
-
console
.log(
value
.toFixed())
-
console
.log(
value
.length)
-
上面这些语句都不会报错,因为value是any类型,所以后面三个操作都有合法的情况,当value是一个对象时,访问name属性是没问题的;当value是数值类型的时候,调用它的toFixed方法没问题;当value是字符串或数组时获取它的length属性是没问题的。
而当你指定值为unknown类型的时候,如果没有通过基于控制流的类型断言来缩小范围的话,是不能对它进行任何操作的,关于类型断言,我们后面小节会讲到。总之这里你知道了,unknown类型的值不是可以随便操作的。
我们这里只是先来了解unknown和any的区别,unknown还有很多复杂的规则,但是涉及到很多后面才学到的知识,所以需要我们学习了高级类型之后才能再讲解。
2.2.7 拓展阅读
这要讲的不是TypeScript中新增的基本类型,而是高级类型中的两个比较常用类型:联合类型和交叉类型。我们之所以要提前讲解,是因为它俩比较简单,而且很是常用,所以我们先来学习下。
(1) 交叉类型
交叉类型就是取多个类型的并集,使用 &
符号定义,被&符链接的多个类型构成一个交叉类型,表示这个类型同时具备这几个连接起来的类型的特点,来看例子:
-
const merge = <T, U>(arg1: T, arg2: U): T &
U => {
-
let res = <T & U>{};
// 这里指定返回值的类型兼备T和U两个类型变量代表的类型的特点
-
res =
Object.assign(arg1, arg2);
// 这里使用Object.assign方法,返回一个合并后的对象;
-
// 关于该方法,请在例子下面补充中学习
-
return res;
-
};
-
const info1 = {
-
name:
"lison"
-
};
-
const info2 = {
-
age:
18
-
};
-
const lisonInfo = merge(info1, info2);
-
-
console.log(lisonInfo.address);
// error 类型“{ name: string; } & { age: number; }”上不存在属性“address”
-
补充阅读:Object.assign方法可以合并多个对象,将多个对象的属性添加到一个对象中并返回,有一点要注意的是,如果属性值是对象或者数组这种保存的是内存引用的引用类型,会保持这个引用,也就是如果在Object.assign返回的的对象中修改某个对象属性值,原来用来合并的对象也会受到影响。
可以看到,传入的两个参数分别是带有属性 name 和 age 的两个对象,所以它俩的交叉类型要求返回的对象既有 name 属性又有 age 属性。
(2) 联合类型
联合类型在前面课时中几次提到,现在我们来看一下。联合类型实际是几个类型的结合,但是和交叉类型不同,联合类型是要求只要符合联合类型中任意一种类型即可,它使用 |
符号定义。当我们的程序具有多样性,元素类型不唯一时,即使用联合类型。
-
const getLength = (content:
string |
number):
number => {
-
if (
typeof content ===
"string")
return content.length;
-
else
return content.toString().length;
-
};
-
console.log(getLength(
"abc"));
// 3
-
console.log(getLength(
123));
// 3
-
这里我们指定参数既可以是字符串类型也可以是数值类型,这个getLength函数的定义中,其实还涉及到一个知识点,就是类型保护,就是typeof content === “string”
,后面进阶部分我们会学到。
补充说明
有一个问题我需要在这里提前声明一下,以免你在自己联系专栏中例子的时候遇到困惑。在讲解语法知识的时候,会有很多例子,在定义一些类型值,比如枚举,或者后面讲的接口等的时候,对于他们的命名我并不会考虑重复性,比如我这里讲枚举的定义定义了一个名字叫Status的枚举值,在别处我又定义了一个同名的接口,那这个时候你可能会看到如下这种错误提示:
枚举声明只能与命名空间或其他枚举声明合并
正如你看到的,这里这个错误,是因为你在同一个文件不同地方、或者不同文件中,定义了相同名称的值,而由于TypeScript的声明合并策略,他会将同名的一些可合并的声明进行合并,当同名的两个值或类型不能合并的时候,就会报错;或者可以合并的连个同名的值不符合要求,也会有问题。关于声明合并和哪些声明可以合并,以及声明需要符合的条件等我们会在后面章节学到。这里你只要知道,类似于这种报错中提到“声明合并”的或者无法重新声明块范围变量,可能都是因为有相同名称的定义。
小结
本小节我们学习了六个TypeScript中新增的数据类型,它们是:元组、枚举、Any、void、never和unknown,其中枚举我们会在后面一个单独的小节进行详细学习,unknown会在我们学习了高级类型之后再补充。我们还学习了两个简单的高级类型:联合类型和交叉类型。我们还学习了any类型与never类型和unknown类型相比的区别,简单来说,any和never的概念是对立的,而any和unknown类型相似,但是unknown与any相比是较为安全的类型,它并不允许无条件地随意操作。我们学习的联合类型和交叉类型,是各种类型的结合,我们可以使用几乎任何类型,来组成联合类型和交叉类型。
下个小节我们将详细学习Symbol的所有知识,Symbol是ES6标准提出的新概念,TypeScript已经支持了该语法,下节课我们将进行全面学习。
06 Symbol-ES6新基础类型
06 Symbol-ES6新基础类型
更新时间:2019-07-29 12:26:55
你若要喜爱你自己的价值,你就得给世界创造价值。——歌德
symbol
是 ES6 新增的一种基本数据类型,它和 number、string、boolean、undefined 和 null 是同类型的,object 是引用类型。它用来表示独一无二的值,通过 Symbol 函数生成。
本小节代码都是纯JavaScript代码,建议在非TypeScript环境练习,你可以在浏览器开发者工具的控制台里练习。但是因为TypeScript也支持Symbol,所以如果需要特别说明的地方,我们会提示在TypeScript中需要注意的内容。
我们先来看例子:
-
const s =
Symbol();
-
typeof s;
// 'symbol'
-
我们使用Symbol
函数生成了一个 symbol 类型的值 s。
注意:Symbol 前面不能加
new
关键字,直接调用即可创建一个独一无二的 symbol 类型的值。
我们可以在使用 Symbol 方法创建 symbol 类型值的时候传入一个参数,这个参数需要是字符串的。如果传入的参数不是字符串,会先调用传入参数的 toString 方法转为字符串。先来看例子:
-
const s1 =
Symbol(
"lison");
-
const s2 =
Symbol(
"lison");
-
console.log(s1 === s2);
// false
-
// 补充:这里第三行代码可能会报一个错误:This condition will always return 'false' since the types 'unique symbol' and 'unique symbol' have no overlap.
-
// 这是因为编译器检测到这里的s1 === s2始终是false,所以编译器提醒你这代码写的多余,建议你优化。
-
上面这个例子中使用 Symbol 方法创建了两个 symbol 值,方法中都传入了相同的字符串’lison’,但是s1 === s2
却是 false,这就是我们说的,Symbol 方法会返回一个独一无二的值,这个值和任何一个值都不等,虽然我们传入的标识字符串都是”lison”,但是确实两个不同的值。
你可以理解为我们每一个人都是独一无二的,虽然可以有相同的名字,但是名字只是用来方便我们区分的,名字相同但是人还是不同的。Symbol 方法传入的这个字符串,就是方便我们在控制台或程序中用来区分 symbol 值的。我们可以调用 symbol 值的toString
方法将它转为字符串:
-
const s1 =
Symbol(
"lison");
-
console.log(s1.toString());
// 'Symbol(lison)'
-
你可以简单地理解 symbol 值为字符串类型的值,但是它和字符串有很大的区别,它不可以和其他类型的值进行运算,但是可以转为字符串和布尔类型值:
-
let s =
Symbol(
"lison");
-
console.log(s.toString());
// 'Symbol(lison)'
-
console.log(
Boolean(s));
// true
-
console.log(!s);
// false
-
通过上面的例子可以看出,symbol 类型值和对象相似,本身转为布尔值为 true,取反为 false。
2.3.1 作为属性名
在 ES6 中,对象的属性名支持表达式,所以你可以使用一个变量作为属性名,这对于一些代码的简化很有用处,但是表达式必须放到方括号内:
-
let prop =
"name";
-
const obj = {
-
[prop]:
"Lison"
-
};
-
console.log(obj.name);
// 'Lison'
-
了解了这个新特性后,我们接着学习。symbol 值可以作为属性名,因为 symbol 值是独一无二的,所以当它作为属性名时,不会和其他任何属性名重复:
-
let name =
Symbol();
-
let obj = {
-
[name]:
"lison"
-
};
-
console.log(obj);
// { Symbol(): 'lison' }
-
你可以看到,打印出来的对象有一个属性名是 symbol 值。如果我们想访问这个属性值,就只能使用 name 这个 symbol 值:
-
console.log(obj[
name]);
// 'lison'
-
console.log(obj.
name);
// undefined
-
通过上面的例子可以看到,我们访问属性名为 symbol 类型值的 name 时,我们不能使用点’.‘号访问,因为obj.name
这的name
实际上是字符串’name’
,这和访问普通字符串类型的属性名一样。你必须使用方括号的形式,这样obj[name]
这的 name 才是我们定义的 symbol 类型的变量name
,之后我们再访问 obj 的[name]属性就必须使用变量 name。
等我们后面学到 ES6 的类(Class)的时候,会利用此特性实现私有属性和私有方法。
2.3.2 属性名的遍历
使用 Symbol 类型值作为属性名,这个属性不会被for…in
遍历到,也不会被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
获取到:
-
const name =
Symbol(
"name");
-
const obj = {
-
[name]:
"lison",
-
age:
18
-
};
-
for (
const key
in obj) {
-
console.log(key);
-
}
-
// => 'age'
-
console.log(
Object.keys(obj));
-
// ['age']
-
console.log(
Object.getOwnPropertyNames(obj));
-
// ['age']
-
console.log(
JSON.stringify(obj));
-
// '{ "age": 18 }'
-
虽然这么多方法都无法遍历和访问到 Symbol 类型的属性名,但是 Symbol 类型的属性并不是私有属性。我们可以使用Object.getOwnPropertySymbols
方法获取对象的所有symbol类型的属性名:
-
const name =
Symbol(
"name");
-
const obj = {
-
[name]:
"lison",
-
age:
18
-
};
-
const SymbolPropNames =
Object.getOwnPropertySymbols(obj);
-
console.log(SymbolPropNames);
-
// [ Symbol(name) ]
-
console.log(obj[SymbolPropNames[
0]]);
-
// 'lison'
-
// 如果最后一行代码这里报错提示:元素隐式具有 "any" 类型,因为类型“{ [name]: string; age: number; }”没有索引签名。 那可能是在tsconfig.json里开启了noImplicitAny。因为这里我们还没有学习接口等高级类型,所以你可以先忽略这个错误,或者关闭noImplicitAny。
-
除了Object.getOwnPropertySymbols
这个方法,还可以用 ES6 新提供的 Reflect 对象的静态方法Reflect.ownKeys
,它可以返回所有类型的属性名,所以 Symbol 类型的也会返回。
-
const name =
Symbol(
"name");
-
const obj = {
-
[name]:
"lison",
-
age:
18
-
};
-
console.log(
Reflect.ownKeys(obj));
-
// [ 'age', Symbol(name) ]
-
2.3.3 Symbol.for()和 Symbol.keyFor()
Symbol 包含两个静态方法,for
和 keyFor
。
(1) Symbol.for()
我们使用 Symbol 方法创建的 symbol 值是独一无二的,每一个值都不和其他任何值相等,我们来看下例子:
-
const s1 =
Symbol(
"lison");
-
const s2 =
Symbol(
"lison");
-
const s3 =
Symbol.for(
"lison");
-
const s4 =
Symbol.for(
"lison");
-
s3 === s4;
// true
-
s1 === s3;
// false
-
// 这里还是会报错误:This condition will always return 'false' since the types 'unique symbol' and 'unique symbol' have no overlap.还是我们说过的,因为这里的表达式始终是true和false,所以编译器会提示我们。
-
直接使用 Symbol 方法,即便传入的字符串是一样的,创建的 symbol 值也是互不相等的。**而使用 Symbol.for
方法传入字符串,会先检查有没有使用该字符串调用 Symbol.for 方法创建的 symbol 值,如果有,返回该值,如果没有,则使用该字符串新创建一个。**使用该方法创建 symbol 值后会在全局范围进行注册。
注意:这个注册的范围包括当前页面和页面中包含的 iframe,以及 service sorker,我们来看个例子:
-
const iframe =
document.createElement(
"iframe");
-
iframe.src =
String(
window.location);
-
document.body.appendChild(iframe);
-
-
iframe.contentWindow.
Symbol.
for(
"lison") ===
Symbol.
for(
"lison");
//
true
-
// 注意:如果你在JavaScript环境中这段代码是没有问题的,但是如果在TypeScript开发环境中,可能会报错:类型“Window”上不存在属性“
Symbol”。
-
// 因为这里编译器推断出iframe.contentWindow是Window类型,但是TypeScript的声明文件中,对Window的定义缺少
Symbol这个字段,所以会报错,所以你可以这样写:
-
// (iframe.contentWindow
as Window & { Symbol: SymbolConstructor }).
Symbol.
for(
"lison") ===
Symbol.
for(
"lison")
-
// 这里用到了类型断言和交叉类型,SymbolConstructor是内置的类型。
-
上面这段代码的意思是创建一个 iframe
节点并把它放到 body
中,我们通过这个iframe
对象的 contentWindow
拿到这个 iframe
的 window
对象,在iframe.contentWindow
上添加一个值就相当于你在当前页面定义一个全局变量一样,我们看到,在iframe
中定义的键为’lison’的 symbol 值在和在当前页面定义的键为’lison’的 symbol 值相等,说明它们是同一个值。
(2) Symbol.keyFor()
该方法传入一个 symbol 值,返回该值在全局注册的键名:
-
const sym =
Symbol.for(
"lison");
-
console.log(
Symbol.keyFor(sym));
// 'lison'
-
2.3.4 11 个内置 symbol 值
ES6 提供了 11 个内置的 Symbol 值,指向 JS 内部使用的属性和方法。看到它们第一眼你可能会有疑惑,这些不是 Symbol 对象的一个属性值吗?没错,这些内置的 Symbol 值就是保存在 Symbol 上的,你可以把Symbol.xxx
看做一个 symbol 值。接下来我们来挨个学习一下:
(1) Symbol.hasInstance
对象的 Symbol.hasInstance 指向一个内部方法,当你给一个对象设置以 Symbol.hasInstance 为属性名的方法后,当其他对象使用 instanceof 判断是否为这个对象的实例时,会调用你定义的这个方法,参数是其他的这个对象,来看例子:
-
const obj = {
-
[
Symbol.hasInstance](
otherObj) {
-
console.log(otherObj);
-
}
-
};
-
console.log({ a: "a" } instanceof obj); // false
-
// 注意:在TypeScript中这会报错,"instanceof" 表达式的右侧必须属于类型 "any",或属于可分配给 "Function" 接口类型的类型。
-
// 是要求你instanceof操作符右侧的值只能是构造函数或者类,或者类型是any类型。这里你可以使用类型断言,将obj改为obj as any
-
可以看到当我们使用 instanceof 判断{ a: ‘a’ }是否是 obj 创建的实例的时候,Symbol.hasInstance 这个方法被调用了。
(2) Symbol.isConcatSpreadable
这个值是一个可读写布尔值,其值默认是undefined,当一个数组的 Symbol.isConcatSpreadable 设为 true或者为默认的undefined 时,这个数组在数组的 concat 方法中会被扁平化。我们来看下例子:
-
let arr = [
1,
2];
-
console.log([].concat(arr, [
3,
4]));
// 打印结果为[1, 2, 3, 4],length为4
-
let arr1 = [
"a",
"b"];
-
console.log(arr1[
Symbol.isConcatSpreadable]);
// undefined
-
arr1[
Symbol.isConcatSpreadable] =
false;
-
console.log(arr1[
Symbol.isConcatSpreadable]);
// false
-
console.log([].concat(arr1, [
3,
4]));
// 打印结果如下:
-
/
-
[ ["a", "b", Symbol(Symbol.isConcatSpreadable): false], 3, 4 ]
-
最外层这个数组有三个元素,第一个是一个数组,因为我们设置了arr1[Symbol.isConcatSpreadable] = false
-
所以第一个这个数组没有被扁平化,第一个元素这个数组看似是有三个元素,但你在控制台可以看到这个数组的length为2
-
Symbol(Symbol.isConcatSpreadable): false不是他的元素,而是他的属性,我们知道数组也是对象,所以我们可以给数组设置属性
-
你可以试试如下代码,然后看下打印出来的效果:
-
let arr = [1, 2]
-
arr.props = 'value'
-
console.log(arr)
-
/
-
(3) Symbol.species
这里我们需要提前使用类的知识来讲解这个 symbol 值的用法,类的详细内容我们会在后面课程里全面讲解。这个知识你需要在纯JavaScript的开发环境中才能看出效果,你可以在浏览器开发者工具的控制台尝试。在TypeScript中,下面两个例子都是一样的会报a.getName is not a function错误。
首先我们使用 class 定义一个类 C,使用 extends 继承原生构造函数 Array,那么类 C 创建的实例就能继承所有 Array 原型对象上的方法,比如 map、filter 等。我们先来看代码:
-
class C extends Array {
-
getName() {
-
return
"lison";
-
}
-
}
-
const c =
new C(
1,
2,
3);
-
const a = c.map(
item => item +
1);
-
console.log(a);
// [2, 3, 4]
-
console.log(a
instanceof C);
// true
-
console.log(a
instanceof
Array);
// true
-
console.log(a.getName());
// "lison"
-
这个例子中,a 是由 c 通过 map 方法衍生出来的,我们也看到了,a 既是 C 的实例,也是 Array 的实例。但是如果我们想只让衍生的数组是 Array 的实例,就需要用 Symbol.species,我们来看下怎么使用:
-
class C extends Array {
-
static get [
Symbol.species]() {
-
return
Array;
-
}
-
getName() {
-
return
"lison";
-
}
-
}
-
const c =
new C(
1,
2,
3);
-
const a = c.map(
item => item +
1);
-
console.log(a);
// [2, 3, 4]
-
console.log(a
instanceof C);
// false
-
console.log(a
instanceof
Array);
// true
-
console.log(a.getName());
// error a.getName is not a function
-
就是给类 C 定义一个静态 get 存取器方法,方法名为 Symbol.species,然后在这个方法中返回要构造衍生数组的构造函数。所以最后我们看到,a instanceof C
为 false,也就是 a 不再是 C 的实例,也无法调用继承自 C 的方法。
(4) Symbol.match、Symbol.replace、Symbol.search 和 Symbol.split
这个 Symbol.match 值指向一个内部方法,当在字符串 str 上调用 match 方法时,会调用这个方法,来看下例子:
-
let obj = {
-
[
Symbol.match](
string) {
-
return string.length;
-
}
-
};
-
console.log("abcde".match(obj)); // 5
-
相同的还有 Symbol.replace、Symbol.search 和 Symbol.split,使用方法和 Symbol.match 是一样的。
(5) Symbol.iterator
数组的 Symbol.iterator 属性指向该数组的默认遍历器方法:
-
const arr = [1, 2, 3];
-
const iterator = arr[
Symbol.iterator](
);
-
console.log(iterator);
-
console.log(iterator.next());
-
console.log(iterator.next());
-
console.log(iterator.next());
-
console.log(iterator.next());
-
这个 Symbol.iterator 方法是可写的,我们可以自定义遍历器方法。
(6) Symbol.toPrimitive
对象的这个属性指向一个方法,当这个对象被转为原始类型值时会调用这个方法,这个方法有一个参数,是这个对象被转为的类型,我们来看下:
-
let obj = {
-
[
Symbol.toPrimitive](
type) {
-
console.log(type);
-
}
-
};
-
// const b = obj++ // number
-
const a =
abc${obj}
; // string
-
(7) Symbol.toStringTag
Symbol.toStringTag 和 Symbol.toPrimitive 相似,对象的这个属性的值可以是一个字符串,也可以是一个存取器 get 方法,当在对象上调用 toString 方法时调用这个方法,返回值将作为”[object xxx]”中 xxx 这个值:
-
let obj = {
-
[Symbol.toStringTag]: "lison"
-
};
-
obj.toString(); // "[object lison]"
-
let obj2 = {
-
get [
Symbol.toStringTag](
) {
-
return "haha";
-
}
-
};
-
obj2.toString(); // "[object haha]"
-
(8) Symbol.unscopables
这个值和 with 命令有关,我们先来看下 with 怎么使用:
-
const obj = {
-
a:
"a",
-
b:
"b"
-
};
-
with (obj) {
-
console.log(a);
// "a"
-
console.log(b);
// "b"
-
}
-
// 如果是在TypeScript开发环境中,这段代码可能with会报错:不支持 "with" 语句,这是因为在严格模式下,是不允许使用with的。
-
可以看到,使用 with 传入一个对象后,在代码块中访问对象的属性就不需要写对象了,直接就可以用它的属性。对象的 Symbol.unscopables 属性指向一个对象,该对象包含了当使用 with 关键字时,哪些属性被 with 环境过滤掉:
-
console.log(Array.prototype[Symbol.unscopables]);
-
/
-
{
-
copyWithin: true
-
entries: true
-
fill: true
-
find: true
-
findIndex: true
-
includes: true
-
keys: true
-
values: true
-
}
-
/
-
2.3.5 在TypeScript中使用symbol类型
2.3.5.1 基础
学习完ES6标准中Symbol的所有内容后,我们来看下在TypeScript中使用symbol类型值,很简单。就是制定一个值的类型为symbol类型:
-
let sym: symbol =
Symbol()
-
2.3.5.2 unique symbol
TypeScript在2.7版本对Symbol做了补充,增加了unique symbol这种类型,他是symbols的子类型,这种类型的值只能由Symbol()或Symbol.for()创建,或者通过指定类型来指定一个值是这种类型。这种类型的值仅可用于常量的定义和用于属性名。另外还有一点要注意,定义unique symbol类型的值,必须用const不能用let。我们来看个在TypeScript中使用Symbol值作为属性名的例子:
-
const key1: unique symbol =
Symbol()
-
let key2: symbol =
Symbol()
-
const obj = {
-
[key1]:
'value1',
-
[key2]:
'value2'
-
}
-
console.log(obj[key1])
-
console.log(obj[key2])
// error 类型“symbol”不能作为索引类型使用。
-
小结
本小节我们详细学习了Symbol的全部知识,本小节的内容较多:我们学习了Symbol值的基本使用,使用Symbol函数创建一个symbol类型值,可以给它传一个字符串参数,来对symbol值做一个区分,但是即使多次Symbol函数调用传入的是相同的字符串,创建的symbol值也是彼此不同的。
我们还学习了Symbol的两个静态方法:Symbol.for
和Symbol.keyFor
,Symbol.for调用时传入一个字符串,使用此方式创建symbol值时会先在全局范围搜索是否有用此字符串注册的symbol值。如果没有创建一个新的;如果有返回这个symbol值,Symbol.keyFor则是传入一个symbol值然后返回该值在全局注册时的标志字符串。我们还学习了11个内置的symbol值,在设计一些高级逻辑时,可能会用到,大部分业务开发很少用到,你可以了解这些值的用途,日后如果遇到这个需求可以想到这有这些内容。
下个小节我们将对第二个前面大致介绍的知识点——枚举Enum进行详细学习,学完后你将全面了解枚举。
07 深入学习枚举
07 深入学习枚举
更新时间:2019-06-12 16:36:54
立志是事业的大门,工作是登堂入室的旅程。——巴斯德
枚举是 TypeScript 新增加的一种数据类型,这在其他很多语言中很常见,但是 JavaScript 却没有。使用枚举,我们可以给一些难以理解的常量赋予一组具有意义的直观的名字,使其更为直观,你可以理解枚举就是一个字典。枚举使用 enum
关键字定义,TypeScript 支持数字和字符串的枚举。
2.4.1. 数字枚举
我们先来通过数字枚举的简单例子,来看下枚举是做什么的:
-
enum Status {// 这里你的TSLint可能会报一个:枚举声明只能与命名空间或其他枚举声明合并。这样的错误,这个不影响编译,声明合并的问题我们在后面的小节会讲。
-
Uploading,
-
Success,
-
Failed
-
}
-
console.log(Status.Uploading);
//
0
-
console.log(Status[
"Success"]);
//
1
-
console.log(Status.Failed);
//
2
-
我们使用enum
关键字定义了一个枚举值 Status,它包含三个字段,每个字段间用逗号隔开。我们使用枚举值的元素值时,就像访问对象的属性一样,你可以使用’.‘操作符和’[]’两种形式访问里面的值,这和对象一样。
再来看输出的结果,Status.Uploading
是 0,Status['Success']
是 1,Status.Failed
是 2,我们在定义枚举 Status 的时候,并没有指定索引号,是因为这是默认的编号,我们也可以自己指定:
-
//
修改起始编号
-
enum
Color {
-
Red
=
2,
-
Blue,
-
Yellow
-
}
-
console.log(Color.Red,
Color.Blue,
Color.Yellow);
//
2
3
4
-
//
指定任意字段的索引值
-
enum
Status {
-
Success
=
200,
-
NotFound
=
404,
-
Error
=
500
-
}
-
console.log(Status.Success,
Status.NotFound,
Status.Error);
//
200
404
500
-
//
指定部分字段,其他使用默认递增索引
-
enum
Status {
-
Ok
=
200,
-
Created,
-
Accepted,
-
BadRequest
=
400,
-
Unauthorized
-
}
-
console.log(Status.Created,
Status.Accepted,
Status.Unauthorized);
//
201
202
401
-
数字枚举在定义值的时候,可以使用计算值和常量。但是要注意,如果某个字段使用了计算值或常量,那么该字段后面紧接着的字段必须设置初始值,这里不能使用默认的递增值了,来看例子:
-
const getValue = () => {
-
return
0;
-
};
-
enum ErrorIndex {
-
a = getValue(),
-
b,
// error 枚举成员必须具有初始化的值
-
c
-
}
-
enum RightIndex {
-
a = getValue(),
-
b =
1,
-
c
-
}
-
const Start =
1;
-
enum Index {
-
a = Start,
-
b,
// error 枚举成员必须具有初始化的值
-
c
-
}
-
2.4.2. 反向映射
我们定义一个枚举值的时候,可以通过 Enum[‘key’]或者 Enum.key 的形式获取到对应的值 value。TypeScript 还支持反向映射,但是反向映射只支持数字枚举,我们后面要讲的字符串枚举是不支持的。来看下反向映射的例子:
-
enum Status {
-
Success =
200,
-
NotFound =
404,
-
Error =
500
-
}
-
console.log(Status[
"Success"]);
//
200
-
console.log(Status[
200]);
//
'Success'
-
console.log(Status[Status[
"Success"]]);
//
'Success'
-
TypeScript 中定义的枚举,编译之后其实是对象,我们来看下上面这个例子中的枚举值 Status 编译后的样子:
我们可以直接使用tsc指定某个文件或者不指定文件直接编译整个目录,运行后就会产生相应的编译后的JavaScript文件,你也可以到TypeScript官方文档提供的在线练习场,在这里你可以编写TypeScript代码,它会同步进行编译。实时编译为JavaScript代码,是你了解编译后结果的好方式。
-
{
-
200:
"Success",
-
404:
"NotFound",
-
500:
"Error",
-
Error:
500,
-
NotFound:
404,
-
Success:
200
-
}
-
可以看到,TypeScript 会把我们定义的枚举值的字段名分别作为对象的属性名和值,把枚举值的字段值分别作为对象的值和属性名,同时添加到对象中。这样我们既可以通过枚举值的字段名得到值,也可以通过枚举值的值得到字段名。
2.4.3. 字符串枚举
TypeScript2.4 版本新增了字符串枚举,字符串枚举值要求每个字段的值都必须是字符串字面量,或者是该枚举值中另一个字符串枚举成员,先来看个简单例子:
-
enum Message {
-
Error =
"Sorry, error",
-
Success =
"Hoho, success"
-
}
-
console.log(Message.Error);
//
'Sorry, error'
-
再来看我们使用枚举值中其他枚举成员的例子:
-
enum Message {
-
Error =
"error message",
-
ServerError = Error,
-
ClientError = Error
-
}
-
console.log(Message.Error);
//
'error message'
-
console.log(Message.ServerError);
//
'error message'
-
注意,这里的其他枚举成员指的是同一个枚举值中的枚举成员,因为字符串枚举不能使用常量或者计算值,所以也不能使用其他枚举值中的成员。
2.4.4. 异构枚举
简单来说异构枚举就是枚举值中成员值既有数字类型又有字符串类型,如下:
-
enum Result {
-
Faild =
0,
-
Success =
"Success"
-
}
-
但是这种如果不是真的需要,不建议使用。因为往往我们将一类值整理为一个枚举值的时候,它们的特点是相似的。比如我们在做接口请求时的返回状态码,如果是状态码都是数值,如果是提示信息,都是字符串,所以在使用枚举的时候,往往是可以避免使用异构枚举的,重点是做好类型的整理。
2.4.5. 枚举成员类型和联合枚举类型
如果枚举值里所有成员的值都是字面量类型的值,那么这个枚举的每个成员和枚举值本身都可以作为类型来使用,先来看下满足条件的枚举成员的值有哪些:
-
不带初始值的枚举成员,例如
enum E { A }
-
值为字符串字面量,例如
enum E { A = ‘a’ }
-
值为数值字面量,或者带有
-
符号的数值字面量,例如enum E { A = 1 }
、enum E { A = -1 }
当我们的枚举值的所有成员的值都是上面这三种情况的时候,枚举值和成员就可以作为类型来用:
(1) 枚举成员类型
我们可以把符合条件的枚举值的成员作为类型来使用,来看例子:
-
enum Animal {
-
Dog =
1,
-
Cat =
2
-
}
-
interface Dog {
-
type: Animal.Dog;
// 这里使用Animal.Dog作为类型,指定接口Dog的必须有一个type字段,且类型为Animal.Dog
-
}
-
interface Cat {
-
type: Animal.Cat;
// 这里同上
-
}
-
let cat1: Cat = {
-
type: Animal.Dog
// error [ts] 不能将类型“Animal.Dog”分配给类型“Animal.Cat”
-
};
-
let dog: Dog = {
-
type: Animal.Dog
-
};
-
(2) 联合枚举类型
当我们的枚举值符合条件时,这个枚举值就可以看做是一个包含所有成员的联合类型,先来看例子:
-
enum Status {
-
Off,
-
On
-
}
-
interface Light {
-
status: Status;
-
}
-
enum Animal {
-
Dog =
1,
-
Cat =
2
-
}
-
const
light1: Light = {
-
status: Animal.Dog /
/ error 不能将类型“Animal.Dog”分配给类型“Status”
-
};
-
const light2: Light = {
-
status: Status.Off
-
};
-
const light3: Light = {
-
status: Status.On
-
};
-
-
上面例子定义接口 Light 的 status 字段的类型为枚举值 Status,那么此时 status 的属性值必须为 Status.Off 和 Status.On 中的一个,也就是相当于status: Status.Off | Status.On
。
2.4.6. 运行时的枚举
枚举在编译成 JavaScript 之后实际是一个对象。这个我们前面讲过了,既然是对象,那么就可以当成对象来使用,我们来看个例子:
-
enum E {
-
A,
-
B
-
}
-
const getIndex = (
enumObj: {
A: number }): number => {
-
return enumObj.A;
-
};
-
console.log(getIndex(E));
//
0
-
上面这个例子要求 getIndex 的参数为一个对象,且必须包含一个属性名为’A’的属性,其值为数值类型,只要有这个属性即可。当我们调用这个函数,把枚举值 E 作为实参传入是可以的,因为它在运行的时候是一个对象,包含’A’这个属性,因为它在运行的时候相当于下面这个对象:
-
{
-
0:
"A",
-
1:
"B",
-
A:
0,
-
B:
1
-
}
-
2.4.7. const enum
我们定义了枚举值之后,编译成 JavaScript 的代码会创建一个对应的对象,这个对象我们可以在程序运行的时候使用。但是如果我们使用枚举只是为了让程序可读性好,并不需要编译后的对象呢?这样会增加一些编译后的代码量。所以 TypeScript 在 1.4 新增 const enum(完全嵌入的枚举),在之前讲的定义枚举的语句之前加上const
关键字,这样编译后的代码不会创建这个对象,只是会从枚举里拿到相应的值进行替换,来看我们下面的定义:
-
enum Status {
-
Off,
-
On
-
}
-
const
enum Animal {
-
Dog,
-
Cat
-
}
-
const status = Status.On;
-
const animal = Animal.Dog;
-
上面的例子编译成 JavaScript 之后是这样的:
-
var Status;
-
(
function(Status) {
-
Status[(Status[
"Off"] =
0)] =
"Off";
-
Status[(Status[
"On"] =
1)] =
"On";
-
})(Status || (Status = {}));
-
var status = Status.On;
-
var animal =
0;
/
Dog
/
-
我们来看下 Status 的处理,先是定义一个变量 Status,然后定义一个立即执行函数,在函数内给 Status 添加对应属性,首先Status[“Off”] = 0
是给Status
对象设置Off
属性,并且值设为 0,这个赋值表达式的返回值是等号右边的值,也就是 0,所以Status[Status[“Off”] = 0] = "Off"
相当于Status[0] = “Off”
。创建了这个对象之后,将 Status 的 On 属性值赋值给 status;再来看下 animal 的处理,我们看到编译后的代码并没有像Status
创建一个Animal
对象,而是直接把Animal.Dog
的值0
替换到了const animal = Animal.Dog
表达式的Animal.Dog
位置,这就是const enum的用法了。
小结
本小节我们学习了两种基本的枚举:数字枚举和字符串枚举,它俩的最主要的区别就是枚举成员值的类型了,数字枚举成员的值必须都是数值类型,而字符串枚举成员的值必须都是字符串。枚举还有一个概念叫反向映射,就是当我们定义了枚举值后,不仅定义了字段到值的映射,同时编译器根据反向映射定义了值到字段的映射。我们还学习了数字枚举和字符串枚举的杂交体——异构枚举,但是很少用,原因也解释过了;枚举值和枚举成员在作为值使用的同时,还可以作为类型使用,但是有三个条件,可以回顾下;最后我们还学习了枚举值在编译后是一个对象,可以在运行时使用,如果我们在运行时用不到,可以在定义枚举时在前面加上const
来选择不生成对象,而是直接将值替换到响应位置。
下个小节我们将学习类型断言,通过类型断言,可以在一些情况告诉 TypeScript 编译器,我们的逻辑是对的,不是类型错误,从而达到预期。
08 使用类型断言达到预期
08 使用类型断言达到预期
更新时间:2019-06-12 16:37:05
书是人类进步的阶梯。
——高尔基
学完前面的小节,你已经学习完了TypeScript的基本类型。从本小节开始,你将开始接触逻辑。在这之前,先来学习一个概念:类型断言。
虽然 TypeScript 很强大,但有时它还是不如我们了解一个值的类型,这时候我们更希望 TypeScript 不要帮我们进行类型检查,而是交给我们自己来,所以就用到了类型断言。类型断言有点像是一种类型转换,它把某个值强行指定为特定类型,我们先看个例子:
-
const getLength = target => {
-
if (target.length) {
-
return target.length;
-
}
else {
-
return target.toString().length;
-
}
-
};
-
这个函数能够接收一个参数,并返回它的长度,我们可以传入字符串、数组或数值等类型的值。如果有 length 属性,说明参数是数组或字符串类型,如果是数值类型是没有 length 属性的,所以需要把数值类型转为字符串然后再获取 length 值。现在我们限定传入的值只能是字符串或数值类型的值:
-
const getLength = (target:
string |
number):
number => {
-
if (target.length) {
// error 报错信息看下方
-
return target.length;
// error 报错信息看下方
-
}
else {
-
return target.toString().length;
-
}
-
};
-
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法,所以现在加了对参数
target
和返回值的类型定义之后就会报错:
-
// 类型
"string | number"上不存在属性
"length"
-
// 类型
"number"上不存在属性
"length"
-
很显然,我们是要做判断的,我们判断如果 target.length 不为 undefined, 说明它是有 length 属性的,但我们的参数是string | number
联合类型,所以在我们开始做判断的时候就会报错。这个时候就要用类型断言,将tagrget
的类型断言成string
类型。它有两种写法,一种是<type>value
,一种是value as type
,下面例子中我们用两种形式都写出来:
-
const getStrLength = (target:
string |
number):
number => {
-
if ((<
string>target).length) {
// 这种形式在JSX代码中不可以使用,而且也是TSLint不建议的写法
-
return (target
as
string).length;
// 这种形式是没有任何问题的写法,所以建议大家始终使用这种形式
-
}
else {
-
return target.toString().length;
-
}
-
};
-
例子的函数体用到了三次target
,前两次都是访问了 target.length 属性,所以都要用类型断言来表明这个地方是 string 类型;而最后的 target 调用了 toString方法,因为 number 和 string 类型的值都有 toString 方法,所以没有报错。
这样虽然没问题了,但是每一处不同值会有不同情况的地方都需要用类型断言,后面讲到高级类型的时候会讲如何使用自定义类型保护来简化这里。
注意了,这两种写法都可以,但是 tslint 推荐使用
as
关键字,而且在 JSX 中只能使用as
这种写法。
小结
本小节我们学习了类型断言的使用。使用类型断言,我们可以告诉编译器某个值确实是我们所认为的值,从而让编译器进行正确的类型推断,让类型检查符合我们的预期。下个小节我们将学习接口,学习了接口后,我们就可以定义几乎所有的数据结构了。
09 使用接口定义几乎任意结构
09 使用接口定义几乎任意结构
更新时间:2019-06-12 16:37:17
低头要有勇气,抬头要有底气。——韩寒
本小节我们来学习接口,正如题目所说的,你可以使用接口定义几乎任意结构,本小节我们先来学习下接口的基本使用方法。
2.6.1. 基本用法
我们需要定义这样一个函数,参数是一个对象,里面包含两个字段:firstName 和 lastName,也就是英文的名和姓,然后返回一个拼接后的完整名字。来看下函数的定义:
-
// 注:这段代码为纯JavaScript代码,请在JavaScript开发环境编写下面代码,在TypeScript环境会报一些类型错误
-
const getFullName =
({ firstName, lastName }) => {
-
return
${firstName} ${lastName}
;
-
};
-
使用时传入参数:
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/222864.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...