TypeScript
2020-11-03 09:55:06 1 举报
AI智能生成
TypeScript笔记
作者其他创作
大纲/内容
迭代器和生成器
可迭代性
当一个对象实现了Symbol.iterator属性时,我们认为它是可迭代的。
一些内置的类型如 Array,Map,Set,String,Int32Array,Uint32Array等都已经实现了各自的Symbol.iterator。 <br>对象上的 Symbol.iterator函数负责返回供迭代的值。
for..of 语句
for..of会遍历可迭代的对象,调用对象上的Symbol.iterator方法。
for..of vs. for..in 语句
for..of和for..in均可迭代一个列表;但是用于迭代的值却不同,<br>for..in迭代的是对象的 键 的列表,而for..of则迭代对象的键对应的值。
另一个区别是for..in可以操作任何对象;它提供了查看对象属性的一种方法。 但是 for..of关注于迭代对象的值。内置对象Map和Set已经实现了Symbol.iterator方法,让我们可以访问它们保存的值。
代码生成
目标为 ES5 和 ES3
当生成目标为ES5或ES3,迭代器只允许在Array类型上使用。 在非数组值上使用 for..of语句会得到一个错误,就算这些非数组值已经实现了Symbol.iterator属性。
编译器会生成一个简单的for循环做为for..of循环
目标为 ECMAScript 2015 或更高
当目标为兼容ECMAScipt 2015的引擎时,编译器会生成相应引擎的for..of内置迭代器实现方式。
模块
介绍
从ECMAScript 2015开始,JavaScript引入了模块的概念。TypeScript也沿用这个概念。
模块在其自身的作用域里执行,而不是在全局作用域里;这意味着定义在一个模块里的变量,函数,类等等在模块外部是不可见的,除非你明确地使用export形式之一导出它们。 相反,如果想使用其它模块导出的变量,函数,类,接口等的时候,你必须要导入它们,可以使用 import形式之一。
模块是自声明的;两个模块之间的关系是通过在文件级别上使用imports和exports建立的。
模块使用模块加载器去导入其它的模块。 在运行时,模块加载器的作用是在执行此模块代码前去查找并执行这个模块的所有依赖。
TypeScript与ECMAScript 2015一样,任何包含顶级import或者export的文件都被当成一个模块。相反地,如果一个文件不带有顶级的import或者export声明,那么它的内容被视为全局可见的(因此对模块也是可见的)。
导出
导出声明
任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export关键字来导出。
导出语句
导出语句很便利,因为我们可能需要对导出的部分重命名
重新导出
重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量。
或者一个模块可以包裹多个模块,并把他们导出的内容联合在一起通过语法:export * from "module"。
导入
导入一个模块中的某个导出内容
可以对导入内容重命名
将整个模块导入到一个变量,并通过它来访问模块的导出部分
具有副作用的导入模块
尽管不推荐这么做,一些模块会设置一些全局状态供其它模块使用。
这些模块可能没有任何的导出或用户根本就不关注它的导出
默认导出
每个模块都可以有一个default导出。 默认导出使用 default关键字标记;并且一个模块只能够有一个default导出。
需要使用一种特殊的导入形式来导入 default导出。default导出十分便利。
类和函数声明可以直接被标记为默认导出。 标记为默认导出的类和函数的名字是可以省略的。
default导出也可以是一个值
export = 和 import = require()
CommonJS和AMD的环境里都有一个exports变量,这个变量包含了一个模块的所有导出内容。
CommonJS和AMD的exports都可以被赋值为一个对象, 这种情况下其作用就类似于 es6 语法里的默认导出,即 export default语法了。<br>虽然作用相似,但是 export default 语法并不能兼容CommonJS和AMD的exports。
为了支持CommonJS和AMD的exports, TypeScript提供了export =语法。
export =语法定义一个模块的导出对象。 这里的对象一词指的是类,接口,命名空间,函数或枚举。
若使用export =导出一个模块,则必须使用TypeScript的特定语法import module = require("module")来导入此模块。
生成模块代码
根据编译时指定的模块目标参数,编译器会生成相应的供Node.js (CommonJS),Require.js (AMD),UMD,SystemJS<br>或ECMAScript 2015 native modules (ES6)模块加载系统使用的代码。
可选的模块加载和其它高级加载场景
编译器会检测是否每个模块都会在生成的JavaScript中用到。 如果一个模块标识符只在类型注解部分使用,并且完全没有在表达式中使用时,就不会生成 require这个模块的代码。 省略掉没有用到的引用对性能提升是很有益的,并同时提供了选择性加载模块的能力。
这种模式的核心是import id = require("...")语句可以让我们访问模块导出的类型。 模块加载器会被动态调用(通过 require),就像下面if代码块里那样。 它利用了省略引用的优化,所以模块只在被需要时加载。 为了让这个模块工作,一定要注意 import定义的标识符只能在表示类型处使用(不能在会转换成JavaScript的地方)。
为了确保类型安全性,我们可以使用typeof关键字。 typeof关键字,当在表示类型的地方使用时,会得出一个类型值,这里就表示模块的类型。
使用其它的JavaScript库
外部模块
我们可以使用顶级的 export声明来为每个模块都定义一个.d.ts文件,但最好还是写在一个大的.d.ts文件里。 <br>我们使用与构造一个外部命名空间相似的方法,但是这里使用 module关键字并且把名字用引号括起来,方便之后import。
现在我们可以/// <reference> node.d.ts并且使用import url = require("url");或import * as URL from "url"加载模块。
外部模块简写
假如你不想在使用一个新模块之前花时间去编写声明,你可以采用声明的简写形式以便能够快速使用它。
简写模块里所有导出的类型将是any。
模块声明通配符
某些模块加载器如SystemJS 和 AMD支持导入非JavaScript内容。 它们通常会使用一个前缀或后缀来表示特殊的加载语法。 模块声明通配符可以用来表示这些情况。
现在你可以就导入匹配"*!text"或"json!*"的内容了。
UMD模块
有些模块被设计成兼容多个模块加载器,或者不使用模块加载器(全局变量)。 它们以 UMD模块为代表。 这些库可以通过导入的形式或全局变量的形式访问。
之后,这个库可以在某个模块里通过导入来使用:
它同样可以通过全局变量的形式使用,但只能在某个脚本(指不带有模块导入或导出的脚本文件)里。
创建模块结构指导
尽可能地在顶层导出
从你的模块中导出一个命名空间就是一个增加嵌套的例子。 虽然命名空间有时候有它们的用处,在使用模块的时候它们额外地增加了一层。 这对用户来说是很不便的并且通常是多余的。
导出类的静态方法也有同样的问题 - 这个类本身就增加了一层嵌套。 除非它能方便表述或便于清晰使用,否则请考虑直接导出一个辅助方法。
如果仅导出单个 class 或 function,使用 export default
就像“在顶层上导出”帮助减少用户使用的难度,一个默认的导出也能起到这个效果。 如果一个模块就是为了导出特定的内容,那么你应该考虑使用一个默认导出。 这会令模块的导入和使用变得些许简单。
如果要导出多个对象,把它们放在顶层里导出
明确地列出导入的名字
使用命名空间导入模式当你要导出大量内容的时候
使用重新导出进行扩展
模块不会像全局命名空间对象那样去 合并。 推荐的方案是 不要去改变原来的对象,而是导出一个新的实体来提供新的功能。
模块里不要使用命名空间
模块具有其自己的作用域,并且只有导出的声明才会在模块外部可见。
命名空间在使用模块时几乎没什么价值。
在组织方面,命名空间对于在全局作用域内对逻辑上相关的对象和类型进行分组是很便利的。
通过将类型有层次地组织在命名空间里,可以方便用户找到与使用那些类型。
模块本身已经存在于文件系统之中,这是必须的
命名空间对解决全局作用域里命名冲突来说是很重要的。
在一个模块里,没有理由两个对象拥有同一个名字。 从模块的使用角度来说,使用者会挑出他们用来引用模块的名字,所以也没有理由发生重名的情况。
危险信号
文件的顶层声明是export namespace Foo { ... } (删除Foo并把所有内容向上层移动一层)
文件只有一个export class或export function (考虑使用export default)
多个文件的顶层具有同样的export namespace Foo { (不要以为这些会合并到一个Foo中!)
命名空间
介绍
“外部模块”现在则简称为“模块”,“内部模块”现在叫做“命名空间”
现在推荐的写法 namespace X {)。
任何使用 module关键字来声明一个内部模块的地方都应该使用namespace关键字来替换。 这就避免了让新的使用者被相似的名称所迷惑。
第一步
所有的验证器都放在一个文件里
命名空间
使用命名空间的验证器
分离到多文件
多文件中的命名空间
当涉及到多文件时,我们必须确保所有编译后的代码都被加载了。
第一种方式,把所有的输入文件编译为一个输出文件,需要使用--outFile标记:<br>编译器会根据源码里的引用标签自动地对输出进行排序。你也可以单独地指定每个文件。
第二种方式,我们可以编译每一个文件(默认方式),那么每个源文件都会对应生成一个JavaScript文件。<br> 然后,在页面上通过 <script>标签把所有生成的JavaScript文件按正确的顺序引进来,
别名
另一种简化命名空间操作的方法是使用import q = x.y.z给常用的对象起一个短的名字。 不要与用来加载模块的 import x = require('name')语法弄混了,这里的语法是为指定的符号创建一个别名。 你可以用这种方法为任意标识符创建别名,也包括导入的模块中的对象。
注意,我们并没有使用require关键字,而是直接使用导入符号的限定名赋值。 这与使用 var相似,但它还适用于类型和导入的具有命名空间含义的符号。 重要的是,对于值来讲, import会生成与原始符号不同的引用,所以改变别名的var值并不会影响原始变量的值。
使用其它的JavaScript库
为了描述不是用TypeScript编写的类库的类型,我们需要声明类库导出的API。 由于大部分程序库只提供少数的顶级对象,命名空间是用来表示它们的一个好办法。
我们称其为声明是因为它不是外部程序的具体实现。 我们通常在 .d.ts里写这些声明。 如果你熟悉C/C++,你可以把它们当做 .h文件。
外部命名空间
流行的程序库D3在全局对象d3里定义它的功能。
它的声明文件使用内部模块来定义它的类型。
命名空间和模块
介绍
这篇文章将概括介绍在TypeScript里使用模块与命名空间来组织代码的方法。 <br>我们也会谈及命名空间和模块的高级使用场景,和在使用它们的过程中常见的陷阱。
使用命名空间
命名空间是位于全局命名空间下的一个普通的带有名字的JavaScript对象。 这令命名空间十分容易使用。<br> 它们可以在多文件中同时使用,并通过 --outFile结合在一起。 命名空间是帮你组织Web应用不错的方式,<br>你可以把所有依赖都放在HTML页面的 <script>标签里。
但就像其它的全局命名空间污染一样,它很难去识别组件之间的依赖关系,尤其是在大型的应用中。
使用模块
像命名空间一样,模块可以包含代码和声明。 不同的是模块可以 声明它的依赖。
模块会把依赖添加到模块加载器上(例如CommonJs / Require.js)。 <br>对于小型的JS应用来说可能没必要,但是对于大型应用,<br>这一点点的花费会带来长久的模块化和可维护性上的便利。 <br>模块也提供了更好的代码重用,更强的封闭性以及更好的使用工具进行优化。
对于Node.js应用来说,模块是默认并推荐的组织代码的方式。
从ECMAScript 2015开始,模块成为了语言内置的部分,应该会被所有正常的解释引擎所支持。<br> 因此,对于新项目来说推荐使用模块做为组织代码的方式。
命名空间和模块的陷阱
这部分我们会描述常见的命名空间和模块的使用陷阱和如何去避免它们。
对模块使用/// <reference>
一个常见的错误是使用/// <reference>引用模块文件,应该使用import。 要理解这之间的区别,我们首先应该弄清编译器是如何根据 import路径(例如,import x from "...";或import x = require("...")里面的...,等等)来定位模块的类型信息的。
编译器首先尝试去查找相应路径下的.ts,.tsx再或者.d.ts。 如果这些文件都找不到,编译器会查找 外部模块声明。 回想一下,它们是在 .d.ts文件里声明的。
不必要的命名空间
TypeScript里模块的一个特点是不同的模块永远也不会在相同的作用域内使用相同的名字。 因为使用模块的人会为它们命名,所以完全没有必要把导出的符号包裹在一个命名空间里。
再次重申,不应该对模块使用命名空间,使用命名空间是为了提供逻辑分组和避免命名冲突。 模块文件本身已经是一个逻辑分组,并且它的名字是由导入这个模块的代码指定,所以没有必要为导出的对象增加额外的模块层。
模块的取舍
就像每个JS文件对应一个模块一样,TypeScript里模块文件与生成的JS文件也是一一对应的。 这会产生一种影响,根据你指定的目标模块系统的不同,你可能无法连接多个模块源文件。 例如当目标模块系统为 commonjs或umd时,无法使用outFile选项,但是在TypeScript 1.8以上的版本能够使用outFile当目标为amd或system。
模块解析
模块解析是指编译器在查找导入模块内容时所遵循的流程。
假设有一个导入语句 import { a } from "moduleA"; 为了去检查任何对 a的使用,编译器需要准确的知道它表示什么,并且需要检查它的定义moduleA。
首先,编译器会尝试定位表示导入模块的文件。 编译器会遵循以下二种策略之一: Classic或Node。 这些策略会告诉编译器到 哪里去查找moduleA。
如果上面的解析失败了并且模块名是非相对的(且是在"moduleA"的情况下),编译器会尝试定位一个外部模块声明。 我们接下来会讲到非相对导入。
最后,如果编译器还是不能解析这个模块,它会记录一个错误。 在这种情况下,错误可能为 error TS2307: Cannot find module 'moduleA'.
相对 vs. 非相对模块导入
根据模块引用是相对的还是非相对的,模块导入会以不同的方式解析。
相对导入是以/,./或../开头的。
所有其它形式的导入被当作非相对的。
相对导入在解析时是相对于导入它的文件,并且不能解析为一个外部模块声明。 你应该为你自己写的模块使用相对导入,这样能确保它们在运行时的相对位置。
非相对模块的导入可以相对于baseUrl或通过下文会讲到的路径映射来进行解析。 它们还可以被解析成 外部模块声明。 使用非相对路径来导入你的外部依赖。
模块解析策略
共有两种可用的模块解析策略:Node和Classic。<br>你可以使用 --moduleResolution标记来指定使用哪种模块解析策略。<br>若未指定,那么在使用了 --module AMD | System | ES2015时的默认值为Classic,其它情况时则为Node。
Classic
这种策略在以前是TypeScript默认的解析策略。 现在,它存在的理由主要是为了向后兼容。
相对导入的模块是相对于导入它的文件进行解析的。
对于非相对模块的导入,编译器则会从包含导入文件的目录开始依次向上级目录遍历,尝试定位匹配的声明文件。
Node
这个解析策略试图在运行时模仿Node.js模块解析机制。 完整的Node.js解析算法可以在 Node.js module documentation找到。
Node.js如何解析模块
为了理解TypeScript编译依照的解析步骤,先弄明白Node.js模块是非常重要的。 通常,在Node.js里导入是通过 require函数调用进行的。<br> Node.js会根据 require的是相对路径还是非相对路径做出不同的行为。
相对路径很简单。假设有一个文件路径为 /root/src/moduleA.js,<br>包含了一个导入var x = require("./moduleB"); Node.js以下面的顺序解析这个导入:
检查/root/src/moduleB.js文件是否存在。
检查/root/src/moduleB目录是否包含一个package.json文件,且package.json文件指定了一个"main"模块。<br> 在我们的例子里,如果Node.js发现文件 /root/src/moduleB/package.json包含了{ "main": "lib/mainModule.js" },<br>那么Node.js会引用/root/src/moduleB/lib/mainModule.js。
检查/root/src/moduleB目录是否包含一个index.js文件。 这个文件会被隐式地当作那个文件夹下的"main"模块。
但是,非相对模块名的解析是个完全不同的过程。
Node会在一个特殊的文件夹 node_modules里查找你的模块。 node_modules可能与当前文件在同一级目录下,或者在上层目录里。 <br>Node会向上级目录遍历,查找每个 node_modules直到它找到要加载的模块。
TypeScript如何解析模块
TypeScript是模仿Node.js运行时的解析策略来在编译阶段定位模块定义文件。<br> 因此,TypeScript在Node解析逻辑基础上增加了TypeScript源文件的扩展名( .ts,.tsx和.d.ts)。<br> 同时,TypeScript在 package.json里使用字段"types"来表示类似"main"的意义 - 编译器会使用它来找到要使用的"main"定义文件。
附加的模块解析标记
有时工程源码结构与输出结构不同。 通常是要经过一系统的构建步骤最后生成输出。 <br>它们包括将 .ts编译成.js,将不同位置的依赖拷贝至一个输出位置。 <br>最终结果就是运行时的模块名与包含它们声明的源文件里的模块名不同。 <br>或者最终输出文件里的模块路径与编译时的源文件路径不同了。
TypeScript编译器有一些额外的标记用来通知编译器在源码编译成最终输出的过程中都发生了哪个转换。
有一点要特别注意的是编译器不会进行这些转换操作; 它只是利用这些信息来指导模块的导入。
Base URL
在利用AMD模块加载器的应用里使用baseUrl是常见做法,它要求在运行时模块都被放到了一个文件夹里。 这些模块的源码可以在不同的目录下,但是构建脚本会将它们集中到一起。
设置baseUrl来告诉编译器到哪里去查找模块。 所有非相对模块导入都会被当做相对于 baseUrl。
baseUrl的值由以下两者之一决定:
命令行中baseUrl的值(如果给定的路径是相对的,那么将相对于当前路径进行计算)
‘tsconfig.json’里的baseUrl属性(如果给定的路径是相对的,那么将相对于‘tsconfig.json’路径进行计算)
注意相对模块的导入不会被设置的baseUrl所影响,因为它们总是相对于导入它们的文件。
路径映射
有时模块不是直接放在baseUrl下面。
TypeScript编译器通过使用tsconfig.json文件里的"paths"来支持这样的声明映射。
请注意"paths"是相对于"baseUrl"进行解析。
如果 "baseUrl"被设置成了除"."外的其它值,比如tsconfig.json所在的目录,那么映射必须要做相应的改变。
通过"paths"我们还可以指定复杂的映射,包括指定多个回退位置。 <br>假设在一个工程配置里,有一些模块位于一处,而其它的则在另个的位置。 构建过程会将它们集中至一处。
利用rootDirs指定虚拟目录
有时多个目录下的工程源文件在编译时会进行合并放在某个输出目录下。 这可以看做一些源目录创建了一个“虚拟”目录。
利用rootDirs,可以告诉编译器生成这个虚拟目录的roots; 因此编译器可以在“虚拟”目录下解析相对模块导入,就 好像它们被合并在了一起一样。
利用rootDirs我们可以让编译器了解这个映射关系,从而也允许编译器能够安全地解析./#{locale}/messages,就算这个目录永远都不存在。
跟踪模块解析
编译器在解析模块时可能访问当前文件夹外的文件。 这会导致很难诊断模块为什么没有被解析,或解析到了错误的位置。 <br>通过 --traceResolution启用编译器的模块解析跟踪,它会告诉我们在模块解析过程中发生了什么。
需要留意的地方
导入的名字及位置
编译器使用的策略
从npm加载types
最终结果
使用--noResolve
正常来讲编译器会在开始编译之前解析模块导入。 每当它成功地解析了对一个文件 import,这个文件被会加到一个文件列表里,以供编译器稍后处理。
--noResolve编译选项告诉编译器不要添加任何不是在命令行上传入的文件到编译列表。 编译器仍然会尝试解析模块,但是只要没有指定这个文件,那么它就不会被包含在内。
常见问题
为什么在exclude列表里的模块还会被编译器使用
如果你想利用 “exclude”排除某些文件,甚至你想指定所有要编译的文件列表,请使用“files”。
有些是被tsconfig.json自动加入的。 它不会涉及到上面讨论的模块解析。<br> 如果编译器识别出一个文件是模块导入目标,它就会加到编译列表里,不管它是否被排除了。
要从编译列表中排除一个文件,你需要在排除它的同时,还要排除所有对它进行import或使用了/// <reference path="..." />指令的文件。
声明合并
介绍
“声明合并”是指编译器将针对同一个名字的两个独立声明合并为单一声明。 <br>合并后的声明同时拥有原先两个声明的特性。<br> 任何数量的声明都可被合并;不局限于两个声明。
基础概念
TypeScript中的声明会创建以下三种实体之一:命名空间,类型或值。
创建命名空间的声明会新建一个命名空间,它包含了用(.)符号来访问时使用的名字。
创建类型的声明是:用声明的模型创建一个类型并绑定到给定的名字上。
创建值的声明会创建在JavaScript输出中看到的值。
合并接口
最简单也最常见的声明合并类型是接口合并。 从根本上说,合并的机制是把双方的成员放到一个同名的接口里。
接口的非函数的成员应该是唯一的。如果它们不是唯一的,那么它们必须是相同的类型。如果两个接口中同时声明了同名的非函数成员且它们的类型不同,则编译器会报错。
对于函数成员,每个同名函数声明都会被当成这个函数的一个重载。 同时需要注意,当接口 A与后来的接口 A合并时,后面的接口具有更高的优先级。
如果签名里有一个参数的类型是 单一的字符串字面量(比如,不是字符串字面量的联合类型),那么它将会被提升到重载列表的最顶端。
合并命名空间
与接口相似,同名的命名空间也会合并其成员。 命名空间会创建出命名空间和值,我们需要知道这两者都是怎么合并的。
对于命名空间的合并,模块导出的同名接口进行合并,构成单一命名空间内含合并后的接口。
对于命名空间里值的合并,如果当前已经存在给定名字的命名空间,那么后来的命名空间的导出成员会被加到已经存在的那个模块里。
除了这些合并外,你还需要了解非导出成员是如何处理的。 非导出成员仅在其原有的(合并前的)命名空间内可见。这就是说合并之后,从其它命名空间合并进来的成员无法访问非导出成员。
命名空间与类和函数和枚举类型合并
命名空间可以与其它类型的声明进行合并。 只要命名空间的定义符合将要合并类型的定义。合并结果包含两者的声明类型。
合并命名空间和类
合并规则与上面 合并命名空间小节里讲的规则一致,我们必须导出 AlbumLabel类,好让合并的类能访问。 合并结果是一个类并带有一个内部类。 你也可以使用命名空间为类增加一些静态属性。
除了内部类的模式,你在JavaScript里,创建一个函数稍后扩展它增加一些属性也是很常见的。 TypeScript使用声明合并来达到这个目的并保证类型安全。
命名空间可以用来扩展枚举型:
非法的合并
TypeScript并非允许所有的合并。 目前,类不能与其它类或变量合并。 想要了解如何模仿类的合并,请参考 TypeScript的混入。
模块扩展
虽然JavaScript不支持合并,但你可以为导入的对象打补丁以更新它们。
它也可以很好地工作在TypeScript中, 但编译器对 Observable.prototype.map一无所知。 你可以使用扩展模块来将它告诉编译器:
模块名的解析和用 import/ export解析模块标识符的方式是一致的。 更多信息请参考 Modules。 当这些声明在扩展中合并时,就好像在原始位置被声明了一样。 但是,你不能在扩展中声明新的顶级声明-仅可以扩展模块中已经存在的声明。
全局扩展
你也以在模块内部添加声明到全局作用域中。
全局扩展与模块扩展的行为和限制是相同的。
JSX
介绍
JSX是一种嵌入式的类似XML的语法。 它可以被转换成合法的JavaScript,尽管转换的语义是依据不同的实现而定的。 JSX因React框架而流行,但也存在其它的实现。 TypeScript支持内嵌,类型检查以及将JSX直接编译为JavaScript。
基本用法
想要使用JSX必须做两件事:
给文件一个.tsx扩展名
启用jsx选项
TypeScript具有三种JSX模式:preserve,react和react-native。
这些模式只在代码生成阶段起作用 - 类型检查并不受影响。
在preserve模式下生成代码中会保留JSX以供后续的转换操作使用(比如:Babel)。 另外,输出文件会带有.jsx扩展名。
react模式会生成React.createElement,在使用前不需要再进行转换操作了,输出文件的扩展名为.js。
react-native相当于preserve,它也保留了所有的JSX,但是输出文件的扩展名是.js。
as操作符
因为TypeScript也使用尖括号来表示类型断言,在结合JSX的语法后将带来解析上的困难。因此,TypeScript在.tsx文件里禁用了使用尖括号的类型断言。
因此我们应该使用另一个类型断言操作符:as。
as操作符在.ts和.tsx里都可用,并且与尖括号类型断言行为是等价的。
类型检查
为了理解JSX的类型检查,你必须首先理解固有元素与基于值的元素之间的区别。 假设有这样一个JSX表达式<expr />,expr可能引用环境自带的某些东西(比如,在DOM环境里的div或span)或者是你自定义的组件。 这是非常重要的,原因有如下两点:
对于React,固有元素会生成字符串(React.createElement("div")),然而由你自定义的组件却不会生成(React.createElement(MyComponent))。
传入JSX元素里的属性类型的查找方式不同。 固有元素属性本身就支持,然而自定义的组件会自己去指定它们具有哪个属性。
固有元素
固有元素使用特殊的接口JSX.IntrinsicElements来查找。 默认地,如果这个接口没有指定,会全部通过,不对固有元素进行类型检查。 然而,如果这个接口存在,那么固有元素的名字需要在JSX.IntrinsicElements接口的属性里查找。
基于值的元素
基于值的元素会简单的在它所在的作用域里按标识符查找。
有两种方式可以定义基于值的元素:
无状态函数组件 (SFC)
类组件
由于这两种基于值的元素在JSX表达式里无法区分,因此TypeScript首先会尝试将表达式做为无状态函数组件进行解析。如果解析成功,那么TypeScript就完成了表达式到其声明的解析操作。如果按照无状态函数组件解析失败,那么TypeScript会继续尝试以类组件的形式进行解析。如果依旧失败,那么将输出一个错误。
无状态函数组件
正如其名,组件被定义成JavaScript函数,它的第一个参数是props对象。 TypeScript会强制它的返回值可以赋值给JSX.Element。
由于无状态函数组件是简单的JavaScript函数,所以我们还可以利用函数重载。
类组件
我们可以定义类组件的类型。 然而,我们首先最好弄懂两个新的术语:元素类的类型和元素实例的类型。
现在有<Expr />,元素类的类型为Expr的类型。 所以在上面的例子里,如果MyComponent是ES6的类,那么类类型就是类的构造函数和静态部分。 如果MyComponent是个工厂函数,类类型为这个函数。
一旦建立起了类类型,实例类型由类构造器或调用签名(如果存在的话)的返回值的联合构成。 再次说明,在ES6类的情况下,实例类型为这个类的实例的类型,并且如果是工厂函数,实例类型为这个函数返回值类型。
元素的实例类型很有趣,因为它必须赋值给JSX.ElementClass或抛出一个错误。 默认的JSX.ElementClass为{},但是它可以被扩展用来限制JSX的类型以符合相应的接口。
属性类型检查
属性类型检查的第一步是确定元素属性类型。 这在固有元素和基于值的元素之间稍有不同。
对于基于值的元素,就稍微复杂些。 它取决于先前确定的在元素实例类型上的某个属性的类型。
至于该使用哪个属性来确定类型取决于JSX.ElementAttributesProperty。
它应该使用单一的属性来定义。 这个属性名之后会被使用。
如果未指定JSX.ElementAttributesProperty,那么将使用类元素构造函数或SFC调用的第一个参数的类型。
元素属性类型用于的JSX里进行属性的类型检查。 支持可选属性和必须属性。
子孙类型检查
从TypeScript 2.3开始,我们引入了children类型检查。children是元素属性(attribute)类型的一个特殊属性(property),子JSXExpression将会被插入到属性里。
如不特殊指定子孙的类型,我们将使用React typings里的默认类型。
JSX结果类型
默认地JSX表达式结果的类型为any。 你可以自定义这个类型,通过指定JSX.Element接口。 然而,不能够从接口里检索元素,属性或JSX的子元素的类型信息。 它是一个黑盒。
嵌入的表达式
JSX允许你使用{ }标签来内嵌表达式。
上面的代码产生一个错误,因为你不能用数字来除以一个字符串。
React整合
要想一起使用JSX和React,你应该使用React类型定义。 这些类型声明定义了JSX合适命名空间来使用React。
工厂函数
jsx: react编译选项使用的工厂函数是可以配置的。可以使用jsxFactory命令行选项,或内联的@jsx注释指令在每个文件上设置。比如,给createElement设置jsxFactory,<div />会使用createElement("div")来生成,而不是React.createElement("div")。
工厂函数的选择同样会影响JSX命名空间的查找(类型检查)。如果工厂函数使用React.createElement定义(默认),编译器会先检查React.JSX,之后才检查全局的JSX。如果工厂函数定义为h,那么在检查全局的JSX之前先检查h.JSX。
装饰器
介绍
装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。
装饰器是一项实验性特性,在未来的版本中可能会发生改变。
若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项:
命令行:
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
装饰器
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
装饰器工厂
如果我们要定制一个修饰器如何应用到一个声明上,我们得写一个装饰器工厂函数。 装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。
装饰器组合
多个装饰器可以同时应用到一个声明上,
书写在同一行上:
书写在多行上:
当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合f和g时,复合的结果(f ∘ g)(x)等同于f(g(x))。
在TypeScript里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:
由上至下依次对装饰器表达式求值。
求值的结果会被当作函数,由下至上依次调用。
装饰器求值
类中不同声明上的装饰器将按以下规定的顺序应用:
参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
参数装饰器应用到构造函数。
类装饰器应用到类。
类装饰器
类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。 类装饰器不能用在声明文件中( .d.ts),也不能用在任何外部上下文中(比如declare的类)。
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。
注意 如果你要返回一个新的构造函数,你必须注意处理好原来的原型链。 在运行时的装饰器调用逻辑中 不会为你做这些。<br>
方法装饰器
方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。 方法装饰器不能用在声明文件( .d.ts),重载或者任何外部上下文(比如declare的类)中。
方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数
对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
成员的名字。
成员的属性描述符。
注意 如果代码输出目标版本小于ES5,属性描述符将会是undefined。
如果方法装饰器返回一个值,它会被用作方法的属性描述符。
如果代码输出目标版本小于ES5返回值会被忽略。
访问器装饰器
访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。 访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。
TypeScript不允许同时装饰一个成员的get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。
访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
成员的名字。
成员的属性描述符。
如果代码输出目标版本小于ES5,Property Descriptor将会是undefined
如果访问器装饰器返回一个值,它会被用作方法的属性描述符。
果代码输出目标版本小于ES5返回值会被忽略。
属性装饰器
属性装饰器声明在一个属性声明之前(紧靠着属性声明)。 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里
属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
成员的名字。
属性描述符不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。 因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。因此,属性描述符只能用来监视类中是否声明了某个名字的属性。
我们可以用它来记录这个属性的元数据,
参数装饰器
参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。 参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如 declare的类)里。
参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
成员的名字。
参数在函数参数列表中的索引。
参数装饰器只能用来监视一个方法的参数是否被传入。
参数装饰器的返回值会被忽略。
元数据
一些例子使用了reflect-metadata库来支持实验性的metadata API。
这个库还不是ECMAScript (JavaScript)标准的一部分。
npm i reflect-metadata --save
TypeScript支持为带有装饰器的声明生成元数据。 <br>你需要在命令行或 tsconfig.json里启用emitDecoratorMetadata编译器选项。
Command Line:
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
当启用后,只要reflect-metadata库被引入了,设计阶段添加的类型信息可以在运行时使用。
TypeScript编译器可以通过@Reflect.metadata装饰器注入设计阶段的类型信息。
装饰器元数据是个实验性的特性并且可能在以后的版本中发生破坏性的改变(breaking changes)。
Mixins
介绍
除了传统的面向对象继承方式,还流行一种通过可重用组件创建类的方式,就是联合另一个简单类的代码。 <br>你可能在Scala等语言里对mixins及其特性已经很熟悉了,但它在JavaScript中也是很流行的。
混入示例
首先应该注意到的是,没使用extends而是使用implements。 把类当成了接口,仅使用Disposable和Activatable的类型而非其实现。 这意味着我们需要在类里面实现接口。 但是这是我们在用mixin时想避免的。
我们可以这么做来达到目的,为将要mixin进来的属性方法创建出占位属性。 这告诉编译器这些成员在运行时是可用的。 这样就能使用mixin带来的便利,虽说需要提前定义一些占位属性。
最后,把mixins混入定义的类,完成全部实现部分。
最后,创建这个帮助函数,帮我们做混入操作。 它会遍历mixins上的所有属性,并复制到目标上去,把之前的占位属性替换成真正的实现代码。
三斜线指令
三斜线指令是包含单个XML标签的单行注释。 注释的内容会做为编译器指令使用。
三斜线指令仅可放在包含它的文件的最顶端。 一个三斜线指令的前面只能出现单行或多行注释,这包括其它的三斜线指令。 如果它们出现在一个语句或声明之后,那么它们会被当做普通的单行注释,并且不具有特殊的涵义。
/// <reference path="..." />指令是三斜线指令中最常见的一种。 它用于声明文件间的 依赖。
三斜线引用告诉编译器在编译过程中要引入的额外的文件。
当使用--out或--outFile时,它也可以做为调整输出内容顺序的一种方法。 文件在输出文件内容中的位置与经过预处理后的输入顺序一致。
预处理输入文件
编译器会对输入文件进行预处理来解析所有三斜线引用指令。 在这个过程中,额外的文件会加到编译过程中。
这个过程会以一些根文件开始; 它们是在命令行中指定的文件或是在 tsconfig.json中的"files"列表里的文件。 这些根文件按指定的顺序进行预处理。 在一个文件被加入列表前,它包含的所有三斜线引用都要被处理,还有它们包含的目标。 三斜线引用以它们在文件里出现的顺序,使用深度优先的方式解析。
一个三斜线引用路径是相对于包含它的文件的,如果不是根文件。
错误
引用不存在的文件会报错。 一个文件用三斜线指令引用自己会报错。
使用 --noResolve
如果指定了--noResolve编译选项,三斜线引用会被忽略;它们不会增加新文件,也不会改变给定文件的顺序。
/// <reference types="..." />
与 /// <reference path="..." />指令相似,这个指令是用来声明 依赖的; 一个 /// <reference types="..." />指令则声明了对某个包的依赖。
对这些包的名字的解析与在 import语句里对模块名的解析类似。 可以简单地把三斜线类型引用指令当做 import声明的包。
把 /// <reference types="node" />引入到声明文件,表明这个文件使用了 @types/node/index.d.ts里面声明的名字; 并且,这个包需要在编译阶段与声明文件一起被包含进来。
对于那些在编译阶段生成的声明文件,编译器会自动地添加/// <reference types="..." />;
当且仅当结果文件中使用了引用的包里的声明时才会在生成的声明文件里添加/// <reference types="..." />语句。
/// <reference no-default-lib="true"/>
这个指令把一个文件标记成默认库。 你会在 lib.d.ts文件和它不同的变体的顶端看到这个注释。
这个指令告诉编译器在编译过程中不要包含这个默认库(比如,lib.d.ts)。 这与在命令行上使用 --noLib相似。
还要注意,当传递了--skipDefaultLibCheck时,编译器只会忽略检查带有/// <reference no-default-lib="true"/>的文件。
/// <amd-module />
默认情况下生成的AMD模块都是匿名的。 但是,当一些工具需要处理生成的模块时会产生问题,比如 r.js。
amd-module指令允许给编译器传入一个可选的模块名:
/// <amd-dependency />
这个指令被废弃了。使用import "moduleName";语句代替。
JavaScript文件类型检查
TypeScript 2.3以后的版本支持使用--checkJs对.js文件进行类型检查和错误提示。
用JSDoc类型表示类型信息
.js文件里,类型可以和在.ts文件里一样被推断出来。 同样地,当类型不能被推断时,它们可以通过JSDoc来指定,就好比在.ts文件里那样。 如同TypeScript,--noImplicitAny会在编译器无法推断类型的位置报错。
JSDoc注解修饰的声明会被设置为这个声明的类型。
属性的推断来自于类内的赋值语句
ES2015没提供声明类属性的方法。属性是动态赋值的,就像对象字面量一样。
在.js文件里,编译器从类内部的属性赋值语句来推断属性类型。 属性的类型是在构造函数里赋的值的类型,除非它没在构造函数里定义或者在构造函数里是undefined或null。 若是这种情况,类型将会是所有赋的值的类型的联合类型。 在构造函数里定义的属性会被认为是一直存在的,然而那些在方法,存取器里定义的属性被当成可选的。
如果一个属性从没在类内设置过,它们会被当成未知的。
如果类的属性只是读取用的,那么就在构造函数里用JSDoc声明它的类型。 如果它稍后会被初始化,你甚至都不需要在构造函数里给它赋值:
构造函数等同于类
ES2015以前,Javascript使用构造函数代替类。 编译器支持这种模式并能够将构造函数识别为ES2015的类。 属性类型推断机制和上面介绍的一致。
支持CommonJS模块
在.js文件里,TypeScript能识别出CommonJS模块。 对exports和module.exports的赋值被识别为导出声明。 相似地,require函数调用被识别为模块导入。
对JavaScript文件里模块语法的支持比在TypeScript里宽泛多了。 大部分的赋值和声明方式都是允许的。
类,函数和对象字面量是命名空间
.js文件里的类是命名空间。 它可以用于嵌套类,
ES2015之前的代码,它可以用来模拟静态方法
它还可以用于创建简单的命名空间
同时还支持其它的变化
对象字面量是开放的
.ts文件里,用对象字面量初始化一个变量的同时也给它声明了类型。 新的成员不能再被添加到对象字面量中。 这个规则在.js文件里被放宽了;对象字面量具有开放的类型,允许添加并访问原先没有定义的属性。
对象字面量的表现就好比具有一个默认的索引签名[x:string]: any,它们可以被当成开放的映射而不是封闭的对象。
与其它JS检查行为相似,这种行为可以通过指定JSDoc类型来改变
null,undefined,和空数组的类型是any或any[]
任何用null,undefined初始化的变量,参数或属性,它们的类型是any,就算是在严格null检查模式下。 任何用[]初始化的变量,参数或属性,它们的类型是any[],就算是在严格null检查模式下。 唯一的例外是像上面那样有多个初始化器的属性。
函数参数是默认可选的
由于在ES2015之前无法指定可选参数,因此.js文件里所有函数参数都被当做是可选的。 使用比预期少的参数调用函数是允许的。
需要注意的一点是,使用过多的参数调用函数会得到一个错误。
使用JSDoc注解的函数会被从这条规则里移除。 使用JSDoc可选参数语法来表示可选性。
由arguments推断出的var-args参数声明
如果一个函数的函数体内有对arguments的引用,那么这个函数会隐式地被认为具有一个var-arg参数(比如:(...arg: any[]) => any))。使用JSDoc的var-arg语法来指定arguments的类型。
未指定的类型参数默认为any
由于JavaScript里没有一种自然的语法来指定泛型参数,因此未指定的参数类型默认为any
在extends语句中:
React.Component被定义成具有两个类型参数,Props和State。 在一个.js文件里,没有一个合法的方式在extends语句里指定它们。默认地参数类型为any:
使用JSDoc的@augments来明确地指定类型。
在JSDoc引用中:
JSDoc里未指定的类型参数默认为any:
在函数调用中
泛型函数的调用使用arguments来推断泛型参数。有时候,这个流程不能够推断出类型,大多是因为缺少推断的源;在这种情况下,类型参数类型默认为any。
支持的JSDoc
下面的列表列出了当前所支持的JSDoc注解,你可以用它们在JavaScript文件里添加类型信息。
@type
可以使用@type标记并引用一个类型名称(原始类型,TypeScript里声明的类型,或在JSDoc里@typedef标记指定的) 可以使用任何TypeScript类型和大多数JSDoc类型。
可以指定联合类型—例如,string和boolean类型的联合。
括号是可选的。
有多种方式来指定数组类型:
还可以指定对象字面量类型。
可以使用字符串和数字索引签名来指定map-like和array-like的对象,使用标准的JSDoc语法或者TypeScript语法。
这两个类型与TypeScript里的{ [x: string]: number }和{ [x: number]: any }是等同的。编译器能识别出这两种语法。
可以使用TypeScript或Closure语法指定函数类型。
或者直接使用未指定的Function类型:
Closure的其它类型也可以使用:
转换
TypeScript借鉴了Closure里的转换语法。 在括号表达式前面使用@type标记,可以将一种类型转换成另一种类型
导入类型
以使用导入类型从其它文件中导入声明。 这个语法是TypeScript特有的,与JSDoc标准不同:
导入类型也可以使用在类型别名声明中
导入类型可以用在从模块中得到一个值的类型。
@param (or @arg or @argument) <br>@returns (or @return)
@param语法和@type相同,但增加了一个参数名。 使用[]可以把参数声明为可选的
函数的返回值类型也是类似的:
@param允许使用相似的语法。 注意,嵌套的属性名必须使用参数名做为前缀
@typedef可以用来声明复杂类型。 和@param类似的语法
@typedef, @callback, 和 @param
可以在第一行上使用object或Object
@callback与@typedef相似,但它指定函数类型而不是对象类型:
当然,所有这些类型都可以使用TypeScript的语法@typedef在一行上声明:
@template
使用@template声明泛型
用逗号或多个标记来声明多个类型参数:
还可以在参数名前指定类型约束。 只有列表的第一项类型参数会被约束:
@class (or @constructor)
编译器通过this属性的赋值来推断构造函数,但你可以让检查更严格提示更友好,你可以添加一个@constructor标记:
通过@constructor,this将在构造函数C里被检查,因此你在initialize方法里得到一个提示,如果你传入一个数字你还将得到一个错误提示。如果你直接调用C而不是构造它,也会得到一个错误。
不幸的是,这意味着那些既能构造也能直接调用的构造函数不能使用@constructor。
@this
编译器通常可以通过上下文来推断出this的类型。但你可以使用@this来明确指定它的类型:
@extends (or @augments)
当JavaScript类继承了一个基类,无处指定类型参数的类型。而@extends标记提供了这样一种方式:
注意@extends只作用于类。当前,无法实现构造函数继承类的情况。
@enum
@enum标记允许你创建一个对象字面量,它的成员都有确定的类型。不同于JavaScript里大多数的对象字面量,它不允许添加额外成员。
注意@enum与TypeScript的@enum大不相同,它更加简单。然而,不同于TypeScript的枚举,@enum可以是任何类型:
已知不支持的模式
在值空间中将对象视为类型是不可以的,除非对象创建了类型,如构造函数。
对象字面量属性上的=后缀不能指定这个属性是可选的:
Nullable类型只在启用了strictNullChecks检查时才启作用:
Non-nullable类型没有意义,以其原类型对待:
不同于JSDoc类型系统,TypeScript只允许将类型标记为包不包含null。 没有明确的Non-nullable -- 如果启用了strictNullChecks,那么number是非null的。 如果没有启用,那么number是可以为null的。
基础类型
布尔值
最基本的数据类型就是简单的true/false值
let isDone: boolean = false;
数字
和JavaScript一样,TypeScript里的所有数字都是浮点数。 这些浮点数的类型是 number。<br> 除了支持十进制和十六进制字面量,TypeScript还支持ECMAScript 2015中引入的二进制和八进制字面量。
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;
字符串
JavaScript程序的另一项基本操作是处理网页或服务器端的文本数据。<br>JavaScript一样,可以使用双引号( ")或单引号(')表示字符串。<br>使用 string表示文本数据类型<br>
let name: string = "bob";
你还可以使用模版字符串,它可以定义多行文本和内嵌表达式<br>这种字符串是被反引号包围( `),并且以${ expr }这种形式嵌入表达式<br>
let name: string = `Gene`;<br>let age: number = 37;<br>let sentence: string = `Hello, my name is ${ name }.<br>I'll be ${ age + 1 } years old next month.`;
数组
可以在元素类型后面接上 []
let list: number[] = [1, 2, 3];
使用数组泛型,Array<元素类型>
let list: Array<number> = [1, 2, 3];
元组 Tuple
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error
当访问一个已知索引的元素,会得到正确的类型
当访问一个越界的元素,会使用联合类型替代
枚举
enum类型是对JavaScript标准数据类型的一个补充,使用枚举类型可以为一组数值赋予友好的名字。<br>
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。或者,全部都采用手动赋值
enum Color {Red = 1, Green, Blue}
enum Color {Red = 1, Green = 2, Blue = 4}
枚举类型提供的一个便利是你可以由枚举的值得到它的名字
Any
当我们想要为那些在编程阶段还不清楚类型的变量指定一个类型的时候,<br>我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。<br>那么我们可以使用 any类型来标记这些变量
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;
在对现有代码进行改写的时候,any类型允许你在编译时可选择地包含或移除类型检查
Void
void类型像是与any类型相反,它表示没有任何类型<br>当一个函数没有返回值时,你通常会见到其返回值类型是 void<br>
声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null:
Null 和 Undefined
undefined和null两者各自有自己的类型分别叫做undefined和null。 和 void相似,它们的本身的类型用处不是很大:
默认情况下null和undefined是所有类型的子类型。 就是说你可以把 null和undefined赋值给number类型的变量。
当你指定了--strictNullChecks标记,null和undefined只能赋值给void和它们各自。
注意:我们鼓励尽可能地使用--strictNullChecks
Never
never类型表示的是那些永不存在的值的类型
never类型是任何类型的子类型,也可以赋值给任何类型
never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型;
没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never。
Object
object表示非原始类型
也就是除number,string,boolean,symbol,null或undefined之外的类型
类型断言
你清楚地知道一个实体具有比它现有类型更确切的类型。
类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。<br>它没有运行时的影响,只是在编译阶段起作用。
“尖括号”语法
let someValue: any = "this is a string";<br>let strLength: number = (<string>someValue).length;
as语法
let strLength: number = (someValue as string).length;
当你在TypeScript里使用JSX时,只有 as语法断言是被允许的。
变量声明
let和const是JavaScript里相对较新的变量声明方式
const是对let的一个增强,它能阻止对一个变量再次赋值。
var 声明
作用域规则
var声明可以在包含它的函数,模块,命名空间或全局作用域内部任何位置被访问(我们后面会详细介绍),包含它的代码块对此没有什么影响。 有些人称此为* var作用域或函数作用域*。 函数参数也使用函数作用域。
捕获变量怪异之处
for (var i = 0; i < 10; i++) {
setTimeout(function() { console.log(i); }, 100 * i);
}
一个通常的解决方法是使用立即执行的函数表达式(IIFE)来捕获每次迭代时i的值
for (var i = 0; i < 10; i++) {<br> (function(i) {<br> setTimeout(function() { console.log(i); }, 100 * i);<br> })(i);<br>}
let 声明
块作用域
当用let声明一个变量,它使用的是词法作用域或块作用域。 <br>不同于使用 var声明的变量那样可以在包含它们的函数外访问,<br>块作用域变量在包含它们的块或for循环之外是不能访问的。
拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。 <br>虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于 暂时性死区。 <br>它只是用来说明我们不能在 let语句之前访问它们,幸运的是TypeScript可以告诉我们这些信息。
我们仍然可以在一个拥有块作用域变量被声明前获取它。 只是我们不能在变量声明前去调用那个函数。<br> 如果生成代码目标为ES2015,现代的运行时会抛出一个错误;然而,现今TypeScript是不会报错的。
重定义及屏蔽
使用var声明时,它不在乎你声明多少次;你只会得到1个。let声明就不会这么宽松了,不能在1个作用域里多次声明
并不是要求两个均是块级作用域的声明TypeScript才会给出一个错误的警告。
并不是说块级作用域变量不能用函数作用域变量来声明。 而是块级作用域变量需要在明显不同的块里声明。
在一个嵌套作用域里引入一个新名字的行为称做屏蔽。
它可能会不小心地引入新问题,
同时也可能会解决一些错误
通常来讲应该避免使用屏蔽,因为我们需要写出清晰的代码。 <br>同时也有些场景适合利用它,你需要好好打算一下。
块级作用域变量的获取
用var声明的变量时
每次进入一个作用域时,它创建了一个变量的 环境。 就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。
当let声明出现在循环体里时拥有完全不同的行为。
不仅是在循环里引入了一个新的变量环境,而是针对 每次迭代都会创建这样一个新作用域。<br> 这就是我们在使用立即执行的函数表达式时做的事
for (let i = 0; i < 10 ; i++) {
setTimeout(function() {console.log(i); }, 100 * i);
}
const 声明
const 声明是声明变量的另一种方式。
与let声明相似,但是就像它的名字所表达的,它们被赋值后不能再改变。
它们拥有与 let相同的作用域规则,但是不能对它们重新赋值。
实际上const变量的内部状态是可修改的。 幸运的是,TypeScript允许你将对象的成员设置成只读的。
let vs. const
使用最小特权原则,所有变量除了你计划去修改的都应该使用const。
基本原则就是如果一个变量不需要对它写入,那么其它使用这些代码的人也不能够写入它们,并且要思考为什么会需要对这些变量重新赋值。
使用 const也可以让我们更容易的推测数据的流动。
解构
解构数组
最简单的解构莫过于数组的解构赋值了:
解构作用于已声明的变量会更好:
作用于函数参数:
你可以在数组里使用...语法创建剩余变量:
对象解构
解构对象:
像数组解构,你可以用没有声明的赋值:
可以在对象里使用...语法创建剩余变量:
属性重命名
可以给属性以不同的名字
let { a: newName1, b: newName2 } = o;
let {a, b}: {a: string, b: number} = o;
默认值
默认值可以让你在属性为 undefined 时使用缺省值:
函数声明
解构也能用于函数声明。
通常情况下更多的是指定默认值,解构默认值有些棘手。
首先,你需要在默认值之前设置其格式。
其次,你需要知道在解构属性上给予一个默认或可选的属性用来替换主初始化列表。
展开
展开操作符正与解构相反。 它允许你将一个数组展开为另一个数组,或将一个对象展开为另一个对象。
对象展开还有其它一些意想不到的限制
首先,它仅包含对象 自身的可枚举属性。
其次,TypeScript编译器不允许展开泛型函数上的类型参数。
接口
介绍
TypeScript的核心原则之一是对值所具有的结构进行类型检查。
它有时被称做“鸭式辨型法”或“结构性子类型化”。
接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。
接口初探
类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。
可选属性
接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。
可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。
带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个<b><font color="#c41230">?</font></b>符号。
可选属性的好处
一是可以对可能存在的属性进行预定义
二是可以捕获引用了不存在的属性时的错误
只读属性
一些对象属性只能在对象刚刚创建的时候修改其值。<br> 你可以在属性名前用 <font color="#c41230">readonly</font>来指定只读属性:
<font color="#c41230">readonly vs const</font>
做为变量使用的话用 const,若做为属性则使用readonly。
额外的属性检查
对象字面量会被特殊对待而且会经过 额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。
绕开这些检查非常简单。 最简便的方法是使用类型断言:
最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性
还有最后一种跳过这些检查的方式,这可能会让你感到惊讶,它就是将这个对象赋值给一个另一个变量:
函数类型
除了描述带有属性的普通对象外,接口也可以描述函数类型。
对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。
函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。
可索引的类型
与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型
可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。
TypeScript支持两种索引签名:字符串和数字。
可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。
这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。
也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。
字符串索引签名能够很好的描述dictionary模式,并且它们也会确保所有属性与其返回值类型相匹配。
因为字符串索引声明了 obj.property和obj["property"]两种形式都可以。
你可以将索引签名设置为只读,这样就防止了给索引赋值:
类类型
实现接口
TypeScript也能够用它来明确的强制一个类去符合某种契约。
也可以在接口中描述一个方法,在类里实现它
接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。
类静态部分与实例部分的区别
类是具有两个类型的:静态部分的类型和实例的类型
当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误
因为当一个类实现了一个接口时,只对其实例部分进行类型检查。
constructor存在于类的静态部分,所以不在检查的范围内
因此,我们应该直接操作类的静态部分。
继承接口
和类一样,接口也可以相互继承。
这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
一个接口可以继承多个接口,创建出多个接口的合成接口。
混合类型
一个对象可以同时做为函数和对象使用,并带有额外的属性。
接口继承类
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。<br>就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。<br>接口同样会继承到类的private和protected成员。<br> 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,<br>这个接口类型只能被这个类或其子类所实现(implement)。
当你有一个庞大的继承结构时这很有用,但要指出的是你的代码只在子类拥有特定属性时起作用。 <br>这个子类除了继承至基类外与基类没有任何关系。
类
介绍
从ECMAScript 2015,也就是ECMAScript 6开始,JavaScript程序员将能够使用基于类的面向对象的方式。 <br>使用TypeScript,我们允许开发者现在就使用这些特性,并且编译后的JavaScript可以在所有主流浏览器和平台上运行,而不需要等到下个JavaScript版本。
类
class 关键字
我们在引用任何一个类成员的时候都用了 this。 它表示我们访问的是类的成员。
使用 new构造了 类的一个实例。 <br>它会调用之前定义的构造函数,创建一个 类型的新对象,并执行构造函数初始化它。
继承
基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。
类从基类中继承了属性和方法。
派生类,它派生自l 基类,通过 extends关键字。
派生类通常被称作 子类,基类通常被称作 超类。
派生类包含了一个构造函数,它 必须调用 super(),它会执行基类的构造函数。
在构造函数里访问 this的属性之前,我们 一定要调用 super()。 这个是TypeScript强制执行的一条重要规则。
公共,私有与受保护的修饰符
默认为 public
理解 private
当成员被标记成 private时,它就不能在声明它的类的外部访问。
TypeScript使用的是结构性类型系统。 <br>当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。
当我们比较带有 private或 protected成员的类型的时候,情况就不同了<br> 如果其中一个类型里包含一个 private成员,那么只有当另外一个类型中也存在这样一个 private成员,<br> 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected成员也使用这个规则。<br>
理解 protected
protected修饰符与 private修饰符的行为很相似,但有一点不同, protected成员在派生类中仍然可以访问。
构造函数也可以被标记成 protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。
readonly修饰符
你可以使用 readonly关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
参数属性
参数属性可以方便地让我们在一个地方定义并初始化一个成员。
参数属性通过给构造函数参数前面添加一个访问限定符来声明。
使用 private限定一个参数属性会声明并初始化一个私有成员;对于 public和 protected来说也是一样。
存取器
TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
对于存取器有下面几点需要注意的:
首先,存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。
其次,只带有 get不带有 set的存取器自动被推断为 readonly。
静态属性
我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。
抽象类
抽象类做为其它派生类的基类使用。
它们一般不会直接被实例化。
不同于接口,抽象类可以包含成员的实现细节。
<font color="#c41230">abstract</font>关键字是用于定义抽象类和在抽象类内部定义抽象方法。
抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。
抽象方法的语法与接口方法相似。
两者都是定义方法签名但不包含方法体。
然而,抽象方法必须包含 abstract关键字并且可以包含访问修饰符。
高级技巧
构造函数
当你在TypeScript里声明了一个类的时候,实际上同时声明了很多东西。 首先就是类的 实例的类型。
我们也创建了一个叫做 构造函数的值。 这个函数会在我们使用 new创建类实例的时候被调用。
我们可以认为类具有 实例部分与 静态部分这两个部分。
把类当做接口使用
类定义会创建两个东西::类的实例类型和一个构造函数。
因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。
函数
介绍
函数是JavaScript应用程序的基础。 它帮助你实现抽象层,模拟类,信息隐藏和模块。
在TypeScript里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义 行为的地方。
TypeScript为JavaScript函数添加了额外的功能,让我们可以更容易地使用。
函数
TypeScript函数可以创建有名字的函数和匿名函数。
在JavaScript里,函数可以使用函数体外部的变量。 当函数这么做时,我们说它‘捕获’了这些变量。
函数类型
为函数定义类型
我们可以给每个参数添加类型之后再为函数本身添加返回值类型。
TypeScript能够根据返回语句自动推断出返回值类型,因此我们通常省略它。
书写完整函数类型
函数类型包含两部分:
参数类型
当写出完整函数类型的时候,这两部分都是需要的。 <br>我们以参数列表的形式写出参数类型,为每个参数指定一个名字和类型。 <br>这个名字只是为了增加可读性。
只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。
返回值类型
对于返回值,我们在函数和返回值类型之前使用( =>)符号,使之清晰明了。
如之前提到的,返回值类型是函数类型的必要部分,如果函数没有返回任何值,你也必须指定返回值类型为 void而不能留空
函数中使用的捕获变量不会体现在类型里。 实际上,这些变量是函数的隐藏状态并不是组成API的一部分。
推断类型
如果赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型
这叫做“按上下文归类”,是类型推论的一种。
可选参数和默认参数
编译器检查用户是否为每个参数都传入了值。 <br>编译器还会假设只有这些参数会被传递进函数。 <br>简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。
JavaScript里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是undefined。
在TypeScript里我们可以在参数名旁使用<font color="#c41230"> ? </font>实现可选参数的功能。
可选参数必须跟在必须参数后面。
我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是undefined时。<br> 它们叫做有默认初始化值的参数。
在所有必须参数后面的带默认初始化的参数都是可选的,与可选参数一样,在调用函数的时候可以省略。 <br>也就是说可选参数与末尾的默认参数共享参数类型。
与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。<br> 如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined值来获得默认值。
剩余参数
必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。
你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 <br>在JavaScript里,你可以使用 arguments来访问所有传入的参数。<br>在TypeScript里,你可以把所有参数收集到一个变量里<br>
剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 <br>编译器创建参数数组,名字是你在省略号( ...)后面给定的名字,你可以在函数体内使用这个数组。
这个省略号也会在带有剩余参数的函数类型定义上使用到
this
this和箭头函数
JavaScript里,this的值在函数被调用的时候才会指定。 <br>这是个既强大又灵活的特点,但是你需要花点时间弄清楚函数调用的上下文是什么。
箭头函数能保存函数创建时的 this值,而不是调用时的值
this参数
this参数在回调函数里
重载
方法是为同一个函数提供多个函数类型定义来进行函数重载。 编译器会根据这个列表去处理函数的调用。
泛型
介绍
可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。
泛型之Hello World
我们需要一种方法使返回值的类型与传入参数的类型是相同的。
我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。类型变量T
T帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。<br> 之后我们再次使用了 T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。<br> 这允许我们跟踪函数里使用的类型的信息。
我们定义了泛型函数后,可以用两种方法使用。
第一种是,传入所有的参数,包含类型参数:
第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型
我们没必要使用尖括号(<>)来明确地传入类型;
编译器可以查看参数的值,然后把T设置为它的类型
类型推论帮助我们保持代码精简和高可读性。<br>如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。
使用泛型变量
必须把这些参数当做是任意或所有类型。
我们把泛型变量T当做类型的一部分使用,而不是整个类型,增加了灵活性
泛型类型
泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样
我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以
我们还可以使用带有调用签名的对象字面量来定义泛型函数
我们可能想把泛型参数当作整个接口的一个参数。 <br>这样我们就能清楚的知道使用的具体是哪个泛型类型。 <br>这样接口里的其它成员也能知道这个参数的类型了。
泛型类
泛型类看上去与泛型接口差不多。 泛型类使用( <>)括起泛型类型,跟在类名后面。
与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。
泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。
泛型约束
我们定义一个接口来描述约束条件。
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型
我们需要传入符合约束类型的值,必须包含必须的属性
在泛型约束中使用类型参数
你可以声明一个类型参数,且它被另一个类型参数所约束
我们需要在这两个类型之间使用约束。
在泛型里使用类类型
在TypeScript使用泛型创建工厂函数时,需要引用构造函数的类类型。
使用原型属性推断并约束构造函数与类实例的关系。
枚举
枚举
使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。
TypeScript支持数字的和基于字符串的枚举。
数字枚举
使用枚举很简单:通过枚举的属性来访问枚举成员,和枚举的名字来访问枚举类型
数字枚举可以被混入到 计算过的和常量成员
不带初始化器的枚举或者被放在第一的位置,或者被放在使用了数字常量或其它常量初始化了的枚举后面。
字符串枚举
在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。
由于字符串枚举没有自增长的行为,字符串枚举可以很好的序列化。
字符串枚举允许你提供一个运行时有意义的并且可读的值,独立于枚举成员的名字。
异构枚举
枚举可以混合字符串和数字成员
除非你真的想要利用JavaScript运行时的行为,否则我们不建议这样做。
计算的和常量成员
每个枚举成员都带有一个值,它可以是 常量或 计算出来的
它是枚举的第一个成员且没有初始化器,这种情况下它被赋予值 0
它不带有初始化器且它之前的枚举成员是一个 数字常量。 这种情况下,当前枚举成员的值为它上一个枚举成员的值加1。
枚举成员使用 常量枚举表达式初始化。<br>常数枚举表达式是TypeScript表达式的子集,它可以在编译阶段求值。 <br>当一个表达式满足下面条件之一时,它就是一个常量枚举表达式
一个枚举表达式字面量(主要是字符串字面量或数字字面量)
一个对之前定义的常量枚举成员的引用(可以是在不同的枚举类型中定义的)
带括号的常量枚举表达式
一元运算符 +, -, ~其中之一应用在了常量枚举表达式
常量枚举表达式做为二元运算符 +, -, *, /, %, <<, >>, >>>, &, |, ^的操作对象。<br> 若常数枚举表达式求值后为 NaN或 Infinity,则会在编译阶段报错。
所有其它情况的枚举成员被当作是需要计算得出的值。
联合枚举与枚举成员的类型
存在一种特殊的非计算的常量枚举成员的子集:字面量枚举成员<br>字面量枚举成员是指不带有初始值的常量枚举成员,或者是值被初始化为<br>
任何字符串字面量(例如: "foo", "bar", "baz")
任何数字字面量(例如: 1, 100)
应用了一元 -符号的数字字面量(例如: -1, -100)
当所有枚举成员都拥有字面量枚举值时,它就带有了一种特殊的语义。
首先,枚举成员成为了类型
另一个变化是枚举类型本身变成了每个枚举成员的 联合。
你只要知道通过联合枚举,类型系统能够利用这样一个事实,它可以知道枚举里的值的集合。
运行时的枚举
枚举是在运行时真正存在的对象。
反向映射
除了创建一个以属性名做为对象成员的对象之外,数字枚举成员还具有了 反向映射,从枚举值到枚举名字。
生成的代码中,枚举类型被编译成一个对象,它包含了正向映射( name -> value)和反向映射( value -> name)。
引用枚举成员总会生成为对属性访问并且永远也不会内联代码。
要注意的是 不会为字符串枚举成员生成反向映射。
const枚举
为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用 const枚举。
常量枚举通过在枚举上使用 const修饰符来定义
常量枚举只能使用常量枚举表达式,并且不同于常规的枚举,它们在编译阶段会被删除。
常量枚举成员在使用的地方会被内联进来。 之所以可以这么做是因为,常量枚举不允许包含计算成员。
外部枚举
外部枚举用来描述已经存在的枚举类型的形状。
外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。<br> 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。
类型推论
介绍
类型是在哪里如何被推断的。
基础
在有些没有明确指出类型的地方,类型推论会帮助提供类型。
大多数情况下,类型推论是直截了当地。
最佳通用类型
当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。
计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。
由于最终的通用类型取自候选类型,有些时候候选类型共享相同的通用类型,但是却没有一个类型能做为所有候选类型的类型。
如果没有找到最佳通用类型的话,类型推断的结果为联合数组类型
上下文类型
TypeScript类型推论也可能按照相反的方向进行。 这被叫做“按上下文归类”。<br>按上下文归类会发生在表达式的类型与所处的位置相关时。
如果上下文类型表达式包含了明确的类型信息,上下文的类型被忽略。
上下文归类会在很多情况下使用到。 通常包含函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。<br> 上下文类型也会做为最佳通用类型的候选类型。
类型兼容性
介绍
TypeScript里的类型兼容性是基于结构子类型的。
结构类型是一种只使用其成员来描述类型的方式。 它正好与名义(nominal)类型形成对比。
<font color="#c41230">在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。<br>这与结构性类型系统不同,它是基于类型的组成结构,且不要求明确地声明。</font>
使用结构类型系统来描述这些类型比使用名义类型系统更好。
关于可靠性的注意事项
TypeScript的类型系统允许某些在编译阶段无法确认其安全性的操作。<br>当一个类型系统具此属性时,被当做是“不可靠”的。<br>TypeScript允许这种不可靠行为的发生是经过仔细考虑的。
开始
TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性。
比较两个函数
相对来讲,在比较原始类型和对象类型的时候是比较容易理解的,问题是如何判断两个函数是兼容的。
类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型
函数参数双向协变
当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。
这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。
实际上,这极少会发生错误,并且能够实现很多JavaScript里的常见模式。
可选参数及剩余参数
比较函数兼容性的时候,可选参数与必须参数是可互换的。
源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。
当一个函数有剩余参数时,它被当做无限个可选参数。
这对于类型系统来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些undefinded。
常见的函数接收一个回调函数并用对于程序员来说是可预知的参数但对类型系统来说是不确定的参数来调用
函数重载
对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。 <br>这确保了目标函数可以在所有源函数可调用的地方调用。
枚举
枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。
类
类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 <br>比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。
类的私有成员和受保护成员
类的私有成员和受保护成员会影响兼容性。 <br>当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。<br> 同样地,这条规则也适用于包含受保护成员实例的类型检查。 <br>这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。
泛型
因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。
对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。 然后用结果类型进行比较
高级主题
子类型与赋值
目前为止,我们使用了“兼容性”,它在语言规范里没有定义。
在TypeScript里,有两种兼容性:子类型和赋值。
它们的不同点在于,赋值扩展了子类型兼容性,增加了一些规则,允许和any来回赋值,以及enum和对应数字值之间的来回赋值。
语言里的不同地方分别使用了它们之中的机制。 实际上,类型兼容性是由赋值兼容性来控制的,即使在implements和extends语句也不例外。
高级类型
交叉类型(Intersection Types)
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用
联合类型(Union Types)
联合类型与交叉类型很有关联,但是使用上却完全不同。
在传统的面向对象语言里,我们可能会将这两种类型抽象成有层级的类型。
联合类型表示一个值可以是几种类型之一。 我们用竖线( |)分隔每个类型,所以 number | string | boolean表示一个值可以是 number, string,或 boolean。
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
类型保护与区分类型<br>(Type Guards and Differentiating Types)
联合类型适合于那些值可以为不同类型的情况。
JavaScript里常用来区分2个可能值的方法是检查成员是否存在。 如之前提及的,我们只能访问联合类型中共同拥有的成员。
用户自定义的类型保护
TypeScript里的 类型保护机制让它成为了现实。 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型谓词
TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。
typeof类型保护
必须要定义一个函数来判断类型是否是原始类型,这太痛苦了
TypeScript可以将它识别为一个类型保护。 也就是说我们可以直接在代码里检查类型了。
instanceof类型保护
instanceof类型保护是通过构造函数来细化类型的一种方式。
instanceof的右侧要求是一个构造函数,TypeScript将细化为:
此构造函数的 prototype属性的类型,如果它的类型不为 any的话
构造签名所返回的类型的联合
可以为null的类型
TypeScript具有两种特殊的类型, null和 undefined
它们分别具有值null和undefined.
默认情况下,类型检查器认为 null与 undefined可以赋值给任何类型。 null与 undefined是所有其它类型的一个有效值。
这也意味着,你阻止不了将它们赋值给其它类型,就算是你想要阻止这种情况也不行。 null的发明者,Tony Hoare,称它为 价值亿万美金的错误。
当你声明一个变量时,它不会自动地包含 null或 undefined。 你可以使用联合类型明确的包含它们
TypeScript会把 null和 undefined区别对待。 string | null, string | undefined和 string | undefined | null是不同的类型。
可选参数和可选属性
使用了 --strictNullChecks,可选参数会被自动地加上 | undefined:
可选属性也会有同样的处理:
类型保护和类型断言
由于可以为null的类型是通过联合类型实现,那么你需要使用类型保护来去除 null。
如果编译器不能够去除 null或 undefined,你可以使用类型断言手动去除。 语法是添加 !后缀: identifier!从 identifier的类型里去除了 null和 undefined:
类型别名
类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
起别名不会新建一个类型 - 它创建了一个新 名字来引用那个类型。 给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。
同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:
我们也可以使用类型别名来在属性里引用自己:
与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。
类型别名不能出现在声明右侧的任何地方。
接口 vs. 类型别名
类型别名可以像接口一样;然而,仍有一些细微差别。
其一,接口创建了一个新的名字,可以在其它任何地方使用。
另一个重要区别是类型别名不能被 extends和 implements(自己也不能 extends和 implements其它类型)
因为 软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。
另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。
字符串字面量类型
字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。
你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。
字符串字面量类型还可以用于区分函数重载:
数字字面量类型
TypeScript还具有数字字面量类型。
枚举成员类型
当每个枚举成员都是用字面量初始化的时候枚举成员是具有类型的。
我们谈及“单例类型”的时候,多数是指枚举成员类型和数字/字符串字面量类型,尽管大多数用户会互换使用“单例类型”和“字面量类型”。
可辨识联合(Discriminated Unions)
你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合的高级模式,它也称做 标签联合或 代数数据类型。
TypeScript则基于已有的JavaScript模式。 它具有3个要素:
具有普通的单例类型属性— 可辨识的特征。
一个类型别名包含了那些类型的联合— 联合。
此属性上的类型保护。
完整性检查
当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。
有两种方式可以实现。
首先是启用 --strictNullChecks并且指定一个返回值类型
第二种方法使用 never类型,编译器用它来进行完整性检查:
多态的 this类型
多态的 this类型表示的是某个包含类或接口的 子类型。 这被称做 F-bounded多态性。 它能很容易的表现连贯接口间的继承
索引类型(Index types)
使用索引类型,编译器就能够检查使用了动态属性名的代码。
索引类型和字符串索引签名
映射类型
这在JavaScript里经常出现,TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。
在映射类型里,新类型以相同的形式去转换旧类型里每个属性。
由映射类型进行推断
注意这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。
预定义的有条件类型
Exclude<T, U> -- 从T中剔除可以赋值给U的类型。
Extract<T, U> -- 提取T中可以赋值给U的类型。
NonNullable<T> -- 从T中剔除null和undefined。
ReturnType<T> -- 获取函数返回值类型。
InstanceType<T> -- 获取构造函数类型的实例类型。
Symbols
介绍
symbol成为了一种新的原生类型,就像number和string一样。
symbol类型的值是通过Symbol构造函数创建的。
Symbols是不可改变且唯一的。
像字符串一样,symbols也可以被用做对象属性的键。
Symbols也可以与计算出的属性名声明相结合来声明对象的属性和类成员。
众所周知的Symbols
除了用户定义的symbols,还有一些已经众所周知的内置symbols。
Symbol.hasInstance
方法,会被instanceof运算符调用。构造器对象用来识别一个对象是否是其实例。
Symbol.isConcatSpreadable
布尔值,表示当在一个对象上调用Array.prototype.concat时,这个对象的数组元素是否可展开。
Symbol.iterator
方法,被for-of语句调用。返回对象的默认迭代器。
Symbol.match
方法,被String.prototype.match调用。正则表达式用来匹配字符串。
Symbol.replace
方法,被String.prototype.replace调用。正则表达式用来替换字符串中匹配的子串。
Symbol.search
方法,被String.prototype.search调用。正则表达式返回被匹配部分在字符串中的索引。
Symbol.species
函数值,为一个构造函数。用来创建派生对象。
Symbol.split
方法,被String.prototype.split调用。正则表达式来用分割字符串。
Symbol.toPrimitive
方法,被ToPrimitive抽象操作调用。把对象转换为相应的原始值。
Symbol.toStringTag
方法,被内置方法Object.prototype.toString调用。返回创建对象时默认的字符串描述。
Symbol.unscopables
对象,它自己拥有的属性会被with作用域排除在外。
0 条评论
下一页