TypeScript 基础入门
TypeScript 基础入门
介绍
什么是 TypeScript
TypeScript
,也称为 TS
,是 JavaScript(JS)
的超集(给 JS
加上了一些附加功能),其中最重要的就是TS
的类型系统,它提供一套全面的对语法定义、使用的约束,来强化 JS 代码的可读性和可维护性。
TypeScript 特点
对比于 javascript
最核心的不同就是:
- 类型编程
- 新的
ES
的标准的实现(预实现的ES
提案),如装饰器、 可选链?.
、空值合并运算符??
(和可选链一起在 TypeScript3.7 中引入)、类的私有成员private
等。除了部分极端不稳定的语法(说的就是你,装饰器)以外,大部分的TS
实现实际上就是未来的ES
语法。
关于 TS
的类型系统,首先要记住很重要的一点:类型系统只在编译时起作用,在编译为js
之后,关于类型的定义会被擦除,因此不会出现在编译之后的.js
文件中。
也就是说即使 TS
编译器在编译阶段检查出你的 TS
代码有各种的类型错误,只要你配置了对应的编译选项,它依旧可以编译为 JS
文件然后执行。比如在 tsconfig.json
中配置:
TypeScript 的类型系统(Coming Soon…)
简单上手
IDE 的选择
TypeScript
最大的优势之一便是增强了编辑器和 IDE
的功能,包括代码补全、接口提示、跳转到定义、重构等。主流的编辑器都支持 TypeScript
,这里推荐使用 Visual Studio Code。
它是一款开源,跨终端的轻量级编辑器,内置了对 TypeScript
的支持。另外它本身也是用 TypeScript 编写的。
下载安装:https://code.visualstudio.com/
安装 TypeScript
通过前文我们知道,我们编写的 .ts
是不能直接运行的, 它是先通过 typescript complier(tsc)
编译为 .js
之后才能执行。
那么如何获取 tsc
呢?
作为 npm
用户,我们可以通过 npm install -g typescript
,安装 typescript
官方提供的包,其中内置 tsc
。
编写和运行 TS 文件
新建一个helloWorld.ts
然后在 commoand
中输入tsc xxx/xxx/helloWorld.ts
。默认情况下,会在源文件的相同目录下出现一个和其同名的.js
文件,比如我们这里就出现了一个helloWorld.js
文件。
然后我们直接运行这个.js
文件即可,node xxx/xxx/helloWorld.js
。
通过 ts-node 直接运行 ts 文件
这里我们通过在命令行输入npm install -g ts-node
全局安装ts-node
这个包,然后直接ts-node xxx/xxx/helloWorld.ts
,就能看到运行 .ts
之后的结果。
注意的是:ts-node
这个库依赖typescript
这个库。
到这里我们大概也看的出来,其实 ts-node
本质上就是先通过 typescript
这个库提供的 ts-compiler
将 .ts
编译成 .js
,然后再通过 node
来执行js
。
如果使用的编辑器是 vscode
,那么你可以安装 Code Runner
插件,然后结合 ts-node
,
就可以直接点击按钮运行对应的 ts
文件了。
如果你想忽略编译异常,直接运行,可以进行如下配置:
关于 ts-node
更多信息可以参考 ts-node 官方文档。
JavaScript 到 TypeScript 的迁移(Coming Soon…)
参考:js-ts 迁移指南
这里常见的有两个方案:
- 逐步将
.js
文件重写为.ts
文件; - 如果重写成本太高,那也可以通过编写类型声明文件,来对
.js
提供类型的支持;
基础类型
JS
中数据类型分为两种, 分别是:原始数据类型(Primitive data types)
和对象/引用(Object types)
数据类型。
其中原始数据类型总共有 7
种,分别是: number, string, boolean, null, undefined
和 ES6
新增的 symbol
和 ES10
新增的 bigInt
。
包装类
除了null, undefined, symbol, bigInt
之外,其余每一个原始基础类型都有一个对应的包装类。
原始类型 | 包装类(引用类型) |
---|---|
number | Number |
string | String |
boolean | Boolean |
理论上,对于原始类型来说,它是没有属性可以访问的。那么如下的代码你是否会产生疑惑:
这是因为js
底层对于原始类型的数据访问属性有一个包装类的机制,就是先将其转换为对应的包装类,然后访问对应的属性或者方法,然后再转换回来。
因此这里我们要区分包装类和原始类型,他们不是一个东西。
然后关于包装类的构造器有两个用法,一个是 new xxx()
,一个是xxx()
,两者一般都能够类型强转,但是结果不同,前者结果是引用值类型,后者是原始值类型。
在 boolean 这一章节中讲解示范中有案例。
boolean
Boolean(xxx)
和new Boolean(xxx)
结果类型是不同的。前者是原始值类型,后者是引用值类型。
number
和JS
一样,TS
里的所有数字都是浮点数或者大整数 。 这些浮点数的类型是number
, 而大整数的类型则是 bigint
。 除了支持十进制和十六进制字面量,TS
还支持ECMAScript 2015
中引入的二进制和八进制字面量。
数字还可以通过_
分隔,这样方便开发者看清楚数字的进制。
string
和JS
一样,可以使用双引号("
)或单引号('
)表示字符串。ES6
语法中还引入了新的特性,就是模板字符串(“),通过反引号括起来,然后其中通过 ${}
来引用变量。
null 和 undefined
在 TS
中, null
类型的值只有 null
一个, undefined
类型的值只有 undefined
一个。null
和undefined
是其他大多数类型的子类型,因此可以直接赋值给别的类型。
然而,当你指定了--strictNullChecks
标记,或者在tsconfig.json
中进行了配置:
null
和undefined
就只能赋值给any
和它们各自的类型(有一个例外是undefined
还可以赋值给void
类型)。 这能避免很多常见的问题。
注意:我们鼓励尽可能地使用
--strictNullChecks
,但在本文中我们假设这个标记是关闭的。
其他常用类型
top type 和 bottom type
top type
和 bottom type
是指一些类型的特征。
top type
类型最典型的例子就是: any, unknown
,表示其他大多数类型能赋值给它。
bottom type
类型最典型的例子有:any, never, null, undefined
,表示它能赋值给其他大多数类型。
这里看的出来,any
既是top type
,又是bottom type
,它基本放弃了TS
的类型检查。
void
其他类型的值不能赋值给void
类型的变量,但是null
和undefined
可以,所以void
一般是代表的是空这个概念。比如函数没有返回值。
any
一般来说,一个变量在声明的一种确切的类型之后,是不允许在赋值为其他别的类型。
但是有时候,我们会想要为那些在定义阶段还不清楚类型的变量指定一个类型。
这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。
那么我们可以使用any
类型来标记这些变量。
当一个变量不赋值,也不声名类型的值,默认就会被看作 any
类型。
对于一个 any
类型的变量,它的属性或者方法的返回值默认情况下都是 any
类型。
因此在编译阶段,你可以任意通过该变量来访问任意属性,这是自由的,但是同时这也是危险的。因为这样会大大提升导致编译为js
之后的各种运行时错误的可能性。
总结一下:
一个值只要是 any
类型,它的属性或者方法的返回值默认情况下也是any
类型,它就会完全跳过 TS
的编译阶段的类型检查。
那么此时此刻,对于这个变量来说和JS
就区别不大了,这样会丧失掉TS
的种种优点,比如降低了代码的可维护性等。因此我们对于any
类型,要善用,但是不能滥用。
array
在 TS
中声明一个数组有多种方式。
类型 + []
数组泛型
关于泛型,在后面细讲,这里只简单用来描述一下数组。
接口定义
关于接口,这里我们只要知道它的一个作用是可以描述一个对象的形状即可,具体细节可以参考 接口章节,这里只简单用来描述一下数组。
在 JS
中, 数组也是一种对象, 因此可以用接口来描述,
类(伪)数组
类数组和真正的数组的核心区别就是,数组的原型对象是Array.prototype
,因此它具备一系列真正数组的特征,比如具有push, splice, shift, unshift...
等方法。
而类数组的原型对象却不是Array.prototype
,但是为什么将其称之为类数组呢,因为类数组具有以下几个特征和真正的数组类似:
- 具有
length
属性; - 按索引方式存储数据;
常见的类数组有:arguments
,NodeList
,HTMLCollection
等。
在 TS
中如何声明一个类数组类型的变量呢?
tuple(元组)
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。
既然本质上也是数组,那么就可以通过 array
的方式来访问元素或者调用array
的api
来操作元组。比如通过下标访问对应元素。
上面的例子看出来了,元组(数组)中的值的顺序和个数都必须和我们声明的类型完全吻合。
越界元素
一般情况下,元组中元素的个数不能和类型中所对应的类型的个数不匹配。如果不匹配就会出现编译错误:
但是我们可以数组中能够改变自身的方法(push, splice...)
来增加或者减少元组中的个数(即使修改后和定义时的类型不符合)。
当然,这些方法的参数必须要满足元组定义时的所有类型的联合类型。
function
函数的定义方式
在 JS
中,函数有两种定义方式:
- 函数表达式
- 函数声明
函数类型的声明方式
一个函数是有输入和输出的,那么在TS
中定义一个函数的类型的时候,也需要分别定义输入和输出的类型。
那么我们如何先声明一个函数类型的变量,然后对应赋值呢?
还可以用接口描述函数的类型,因为在 js
中函数也是一种对象。
可选参数
默认情况下,一个函数所定义的参数都是必须要传的。
如果你在定义函数的时候,对应某些参数觉得不是非必须的,那么这里可以用可选参数。
可选参数必须在所有必选参数的后面。
参数默认值
TS
会将函数中默认值识别为可选参数,还可以绕开可选参数不得在必选参数之前的限制:
这里和ES6
的解构赋值差不多,都是传入undefined
,将触发该参数等于默认值,null
则没有这个效果。
参考:ES6 函数参数默认值
剩余参数
如果你想实现一个 sum
函数,只限制传入的参数的类型:只能是数字,不限制传入参数的个数。功能是将所有数字求和,然后返回。
这里就需要应用到剩余参数的概念。
参数列表中只能有一个剩余参数,并且剩余参数必须在参数列表的最后一个。
函数重载
一般来说,函数名相同,函数参数可以不同,就是重载,JS
中天然支持重载,因为 JS
中对函数入参没有限制。
但是在TS
中却不行。
在未使用重载的形式下,定义一个 reverse
函数,我们希望在定义的时候,能够让TS
知道这个函数的返回值,这样在使用的时候,我们就能得到TS
类型系统帮助。
比如TS
能够通过该函数返回值的类型给出一些语法提示,降低我们编写代码错误的可能性。
下面的这个例子,TS
无法判断该函数的具体返回值是什么类型的, 只有运行之后才知道。
这里我们无法得到IDE
的提示,因为TS
无法得知该返回值是具体的什么类型。
因此我们这里我们需要提供更多的信息去让TS
去推断该函数的返回值类型是什么,请看下面的例子:
这里我们定义了每一种入参对应的返回值类型。
TS
类型系统会根据我们调用该函数时的入参来进行匹配上述多种的定义,并从中找出一种最符合条件的定义。
调用函数时,根据从上往下匹配重载声明的原则,这个函数的调用会命中function reverse(source: string): string
这个声明,因此TS
推断其返回值为string
,因此这里会有语法提示。
TS 函数中的 this
关于JS
中的this
,可以参考 this 详解。
在JS
中,this
的值在函数被调用的时候才会指定。 这是个既强大又灵活的特点,但是你需要花点时间弄清楚函数调用的上下文是什么。
但众所周知,这不是一件很简单的事,尤其是在返回一个函数或将函数当做参数传递的时候。
解决这个错误的方法有很多,这里我们采用 箭头函数 的方式绑定上下文的this
:
当你将一个函数传递到某个库函数里在稍后被调用时,你可能也见到过回调函数里的this
会报错。
因为当回调函数被调用时,它会被当成一个普通函数调用,this
将为undefined
。
下文采用的方案在函数的参数列表中定义一个this
参数。这样就可以在解决这个问题。
首先,库函数的作者要指定this
的类型:
this: void
意味着addClickListener
期望onclick
是一个函数且它不需要一个this
类型。 然后,为调用代码里的this
添加类型注解:
指定了this
类型后,你显式声明onClickBad
必须在Handler
的实例上调用。 然后TS
会检测到addClickListener
要求函数带有this: void
。 改变this
类型来修复这个错误:
因为onClickGood
指定了this
类型为void
,因此传递addClickListener
是合法的。 当然了,这也意味着不能使用this.info
. 如果你两者都想要,你不得不使用箭头函数了:
这是可行的因为箭头函数使用外层的this
,所以你总是可以把它们传给期望this: void
的函数。 缺点是每个Handler
对象都会创建一个箭头函数。 另一方面,方法只会被创建一次,添加到Handler
的原型链上。 它们在不同Handler
对象间是共享的。
object
object
表示非原始类型,也就是除number
,string
,boolean
,bigint
,symbol
,null
或undefined
之外的类型。
unknown
参考:理解 TypeScript 中 any 和 unknown
当我们在写应用的时候可能会需要描述一个我们还不知道其类型的变量。在这些情况下,我们想要让编译器以及未来的用户知道这个变量可以是任意类型。这个时候我们会对它使用 unknown
类型。
这里看起来它和any
很类似,他们都属于top type。
但他们也是有区别的,比如一个 unknown
类型的变量,如果不对其进行类型的收缩(比如通过 as 进行断言),他是不能进行任何操作的:
类型收缩的常用方式:TS 类型收缩参考
never
never
类型表示的是那些永不存在的值的类型。 例如:
-
总是会抛出异常的函数的返回值类型;
-
根本就不会有返回值的函数的返回值类型;
never
类型的特点:
-
never
是任何类型的子类型, 并且可以赋值给任何类型; -
没有类型是
never
的子类型或者可以赋值给never
(除了never
本身),即使any
也不可以赋值给never
; -
在一个没有返回值标注的函数表达式或箭头函数中,如果函数没有
return
语句,或者仅有表达式类型为never
的return
语句,并且函数的终止点无法被执行到(按照控制流分析),则推导出的函数返回值类型是never
; -
在一个明确指定了
never
返回值类型的函数中, 所有return
语句 (如果有) 表达式的值必须为never
类型,且函数不应能执行到终止点;
关于never
目前我自己也没弄太清楚,大概用法可以参考:TypeScript 中的 never 类型具体有什么用?
将其中的一个例子列举出来,比如当你有一个 联合类型(union type)
:
在switch
当中判断 type
,TS
是可以收窄类型的 (discriminated union)
:
注意在 default
里面我们把被收窄为 never
的 val
赋值给一个显式声明为 never
的变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事改了 All
的类型:
然而他忘记了在 handleValue
里面加上针对 Baz
的处理逻辑,这个时候在 default branch
里面 val
会被收窄为 Baz
,导致无法赋值给 never
,产生一个编译错误。所以通过这个办法,你可以确保 handleValue
总是穷尽 (exhaust)
了所有 All
的可能类型。
高级类型
联合类型
联合类型(Union Types)
,表示一个值可以为多个类型中的一种。
访问联合类型的属性或者方法
由于 TS
在编译期间无法准确判断一个联合类型的值在赋值之前是什么类型,那么我们访问联合类型的值的属性或者方法时,只能访问其公共属性或者方法。
联合类型在被赋值的时候,会被TS
类型推论为其中一种类型。
索引类型查询操作符(keyof)
对于任何类型 T
, keyof T
的结果为 T
上已知的 公共实例属性名(非静态,公共) 的联合(或者看作,字符串字面量类型),因为我们知道一个对象的 key
,除了 symbol
之外,都是 string
类型。
这里还有一个需要关注的点,那就是公共实例属性名 (非静态,公共)这个特征,这个一般指的是class
中的public
修饰的属性(属性默认也是public
的),类是可以看作一个类型的,具体请看类看作接口。
然后可以参考下图:
这里我们就看得出来keyof class
和keyof interface
差不多是等价的。
字符串字面量类型
支持字符串字面量类型(String literal types
),用以限定只有指定的字符串才被允许,使用起来和枚举类型很相似,但前者更轻量一些。
然后当我们调用方法时,可以看到参数只能是我们定义的字符串类型的中的一种:
那么这里如果我们需要设置一个位置的综合怎么办呢,一个位置的具体描述应该是由垂直位置+水平位置的组合而成的:
这里我们发现需要额外定义一个PositionAlignment
的类型,但是定义起来略显繁琐。
下面我们要讲的模板文本类型可以很好的解决这个问题。
模板文本类型
关于模板文本类型:TypeScript4.1 新增模板字符串类型。
类型别名
类型别名指的就是给类型起一个新的名字(映射关系)。
语法是:type xxx = 映射的类型
。
类型别名经常用在 联合类型 中。如下面的例子:
常用内置对象
JS
有许多内置对象(built-in objects
),它们可以直接在 TS
中当做定义好了的类型。
内置对象是指根据标准在全局作用域(Global
)上存在的对象。这里的标准是指 ECMAScript
和其他环境(比如 DOM
)的标准。
ECMA
提供的内置对象:Boolean, Object, Symbol, Function, Error...
等,这里要和原始类型的意思不同,和原始类型的包装类类似。
TS
核心库的定义文件中定义了大多数浏览器环境需要用到的类型,并且是预置在 TS
中的。
比如 String.fromCodePoint
的内置定义:
注意,TS
核心库的定义中不包含 Node.js
部分。
因此如果我们用 TS
写 Node
的时候, 可以 npm install @types/node --save-dev
,从 @types/node
中下载第三方书写的声明文件来获取类型上的提示。
类型推论, 类型断言, 类型别名
类型推论
TS
有一个类型推论机制,在当我们没有显式定义类型的时候,TS
的类型系统会根据现有的条件推导出一个类型。
一个值在声明的时候就会被确定为一个类型(不管是被显式声明,或者被类型推论),后续的赋值并不会影响它的类型
类型断言
类型断言(Type Assertion
) 可以用来手动指定一个值的类型。
有两种语法:
-
值
as
类型; -
<类型>
值: 一般不用;第二种用法一般不用的原因有两个:
-
由于在
tsx
中,<Foo>
也代表一个ReactNode(组件)
, 因此这种用法在tsx
中不能用; -
在
TS
中<xxx>
也可能代表泛型;
-
因此我们一般使用第一种语法来类型断言: 值 as
类型。
将联合类型断言为一种类型
将一个父类断言为一个更具体的子类
将一个父类型断言为一个更为具体的子类型
其实结合后文的类看作接口(类型),你会发现,将父类断言为子类和将父类型断言为子类型是差不多的概念,因为类实际上也能代表类型(interface
)。
将其他类型断言为 any
在理想情况下, TS
的类型系统会运行良好, 每个值的类型都是精确而且具体的。
这里要特别注意, 将一个变量断言为 any
类型, 只能作为我们解决 TS
中类型问题的最后办法,如果滥用 as any
,那么就丧失了 TS
类型检查的意义了。
而我们要做的就是,在类型的严格性和开发的便利性之间掌握平衡。
将 any 断言为一个更具体的类型
比如我们在用别人的方法或者第三方的库时, 发现他们的方法返回值是 any
,我们直接去修改源码不太稳妥, 我们可以在拿到其返回值时将其断言为一个我们需要的类型。
类型断言的限制
这里总结为 2 条(假设这里存在 A 类型和 B 类型):
-
A 能兼容 B,那么 A 就能断言为 B,B 也能被断言为 A
-
B 能兼容 A,那么 B 就能断言为 A,A 也能被断言为 B
也就是 A 类型和 B 类型双方只要能够存在兼容关系,那么双方就能互相断言。
那么兼容到底是什么意思呢?这里我们定义两个兼容的类型作为示范。
这里有一个关键的概念:TS
是结构类型系统,类型之间的对比只会比较它们最终的结构,而会忽略它们定义时的关系。
这里我们看的出来,他们定义的时候并没有继承关系,但是 TS
并不会管他们定义时候的关系, 只会根据其最终的结构来判断其关系, 因此这里会被看作:
这里就能看作 Father
兼容Son
:
总结一下兼容的特征:
- 类型的表示范围上有一方将另外一方包含进去,可以理解为继承,父类型兼容子类型,一般是抽象兼容具体;
- B 类型有的属性或者方法 A 类型一定有, 但是 A 类型有的 B 类型不一定有, 那么就可以看作 B 类型兼容 A类型;
- A 类型的值能够赋值给 B 类型,那么就能理解为 B 类型兼容 A 类型;
因为 Father
兼容Son
,因此他们能够互相断言。
这里关于断言的限制是很有必要的,因为毫无限制的断言是很危险的,会大大增加代码运行时出错的可能性。
所以最后总结一下:
-
联合类型可以被断言为其中一个类型;
-
父类可以被断言为子类(父类型可以被断言为子类型);
-
任何类型都可以被断言为
any
; -
any
可以被断言为任何类型; -
要使得 A 能够被断言为 B,只需要 A 兼容 B 或 B 兼容 A 即可 (前面 4 种分类的本质);
双重断言
any
同时作为 top type 和 bottom type,这意味着any
和其他任意类型都相互兼容(能够赋值给其他大多数类型,其他大多数类型都能赋值给它), 那么就意味着 any
可以与其他任意类型进行相互断言。
慎用!!!
类型断言和类型转换的区别
类型断言只是在编译期间用来对付 TS
的类型系统的,在运行时都会被擦除,因此不能实际影响到运行时。
类型断言和类型声明的区别
这里就看的出来两者的区别:
- 对于类型断言而言,A 类型兼容 B 类型 或者 B 类型兼容 A 类型,两种任意满足一个条件就能使得 A 类型和 B 类型相互断言;
- 对于类型声明而言,只有 A 类型 兼容 B 类型,那么 B 类型才能被赋值给声明为 A 类型的变量;
因此类型声明的条件比类型断言更加严格;一般而言推荐使用类型声明,更加安全和更加优雅。
类型断言和泛型
灵活定义一个函数的返回值类型, 然后根据需要获取确定的返回值类型, 除了用 any
作为返回值类型然后通过类型断言之外的方法外,还有泛型这种方式。
接口
在传统面向对象的语言中,接口(interface
)是一种规范的定义,它定义了行为和动作的规范,在程序设计里面,接口起到一种限制和规范的作用。
接口定义了某一批类所需要遵守的规范,它不关心这些类的内部状态数据,也不关心这些类里方法的实现细节,它只规定这批类里必须提供某些方法,提供这些方法的类就可以满足实际需要。
总结一下:在传统面向对象的语言中,接口一般是对行为的抽象,具体实现交由类去实现(implement
)。
而在 TS
中,接口的概念在原有的基础上被拓展了:
- 对行为进行抽象;
- 通过接口 (
interface
) 来定义(描述)对象的形状(Shape
) — 类型;
描述对象的形状
TS
的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做’鸭式辨型法’或’结构性子类型化’,这种思想是建立在 鸭子类型(duck typing) 上的。
在这里,接口的其中一个作用就是为这些类型命名。
举一个简单的例子:
如果该对象的形状不符合我们接口所定义的,那么TS
会检查出错误:
还有一点就是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以:
可选属性
有些时候,某个接口中的属性不一定全部都是必须的,我们只想根据真实的情况传入部分属性,这个时候就可以利用到可选属性(option bags
)。
可索引的类型
为了方便理解,后面我们都称之为动态(任意)属性
有些时候,我们希望能够根据条件任意传入一个或者多个属性,这个时候,就需要利用到动态属性:
对于动态属性的类型TS
有所限制,这里我们引入一个概念,同类型属性(其他同一类型的属性名)。
对于同类型属性,动态属性对应的值的类型表示的范围要大于等于其它非动态属性的值的类型所表示的范围。这里我们把前者的类型看作 A
后者的类型看作 B
,那么抽象一下就是:B extends A
还能理解为B
类型的值能够赋值给A
类型的值:let param1: A = (param2 as B)
:
动态属性名的类型TS
只支持 4
种,string, number, symbol, 模板文本类型
,关于模板文本类型,具体请参考:模板文本类型。
在 JS
中,我们知道,一个对象的属性只能是字符串,如果传入其他类型的属性值,也会被对应类型的toString
给转换为字符串类型(除了 Symbol
)。
因此当属性名定义为number, string
,能够通过同样的方式获取到。
一个接口可以存在多个动态属性,但是有几个限制:
-
不能存在同类型的动态属性,这里其实也能理解,如果多个同类型的属性的话,
TS
将无法区分这些属性: -
后面定义的动态属性的值的类型必须要能被赋值给前面定义的任意属性的值,也就是前者的类型要兼容后者的类型。假设前者的类型为
A
,后者的类型为B
,let param1: A = (param2 as B)
:
只读属性
有时候我们希望对象中的一些字段只能在创建的时候被赋值,其他地方不能再重新赋值,那么可以用 readonly
定义只读属性:
在定义对象的时候,即使没有给只读属性赋值,TS
也会看作这里有一个undefined
的初始值,在定义之后同样不能再次赋值:
描述函数和数组的形状
这里我们知道,函数和数组其实也是对象的一种:
接口描述混合类型
有时候我们希望一个对象的类型既是函数,同时又具备它自身的属性和方法,比如:jQuery
。在TS
中有很多种方式实现这种效果,这里我们采用用接口来描述这一混合类型的方式。
对行为进行抽象
其实这里从本质上能看作一个接口对一个类的形状的描述。
只不过具体使用由之前的
A:interface
变成了A implements interface
。
接口中定义的方法默认就是抽象方法,不需要abstract
修饰:
抽象方法是不可以有具体实现的:
类
TS
中的类的用法是基于ES6
中类的定义,因此建议先参考:ES6-class。
这里我们着重讲解的是TS
中类相比于ES6
中不同的点。
显式定义属性
访问修饰符
TS
有三种访问修饰符:
public
,在TS
中成员默认就是public
,对访问没有限制;protected
,在自己和子类里面能够访问,其他的外界也无法访问;private
,只有自己里面能够访问,外界都无法访问;
修饰符能够用来修饰属性,方法,构造函数的参数。
一个类的构造器如果被private
修饰,那么意味着它无法被外界实例化,因为被private
修饰的属性或者方法无法在外界被访问到:
只读修饰符
修饰符结合构造函数
通过前文得知,修饰符还能够修饰构造函数的参数,这里其实是一种语法糖:
抽象类
通过 abstract
关键字定义抽象类,同时 abstract
还可以可以定义抽象方法,抽象方法只能定义在抽象类和接口中,接口中的抽象方法是不需要abstract
来修饰的。
抽象类中可以有具体实现的方法。
抽象类无法被实例化,即使它的构造器外界可以访问,子类继承它,就必须要实现抽象类中定义的抽象方法。
类和接口
类的静态部分和实例部分
一个类的属性和方法可以分成两部分,分别是实例部分和静态部分,前者属于类的实例,后者属于类本身(可以看成生命周期不同)。
而类上面关于类型相关一般指的都是类的公共(public
)实例这一部分:
类看作接口
当我们定义一个类的时候,还可以将其当作一个类型(interface
)来使用,这个interface
中有这个类中所有的公共(public
)实例部分。
类继承类
一个类只能直接继承一个类:
继承之后,会分别继承父类的实例部分和静态部分,但是构造器不会被继承。
子类如果没有写构造器,那么默认会自动先调用父类的构造器。
子类如果写了构造器,那么需要在构造器中最先调用父类的构造器。
类实现接口
类实现接口可以看作这个接口描述了这个类的形状,也就是说这个接口描述的形状,该类必须要符合:
一个类可以实现多个接口:
接口继承接口
接口继承接口和类继承类基本没什么区别,因为我们知道类在某种概念上就是一个描述类型的接口(类可以看作接口):
接口继承类
之前我们讲过,类可以看作接口,因此这里就和接口继承接口又差不多了:
泛型
泛型(Generics
),是指在定义类,接口,函数的时候不预先把类型定死。
而是留下传参( <T>
, 这里参数是类型 )入口,在使用类,接口,函数的时候传入指定的类型,这样可以提高灵活性和复用性。
泛型函数
下面来创建第一个使用泛型的例子:identity
函数。 这个函数会返回任何传入它的值。
我们为了这个函数能够支持多种类型的入参,因此将入参的类型和返回值指定为any
。
但是这样就产生了一个很明显的缺陷,就是我们在获取这个函数的返回值时,TS
在编译期间时无法检测出其具体是什么类型,也就是在传参之后,丢失了类型信息。
因此我们需要一种机制能够使返回值的类型与传入参数的类型是相同的,请看下面的例子:
上面我们引入了类型变量的概念,它不是一个实体的概念,编译之后就会被擦除,但是我们依旧可以在编译之前将它当作一个变量来使用,只不过它代表的是一个类型而不是某个实体的值。
类型变量
类型变量有两种使用情况:
-
我们可以使用类似传参的方式,直接给这个类型变量指定某种类型,在后续的地方就可以直接使用这个我们定义好的类型变量:
-
不给类型变量传参,而是让
TS
自己去根据实际情况来推断这个类型变量是什么类型:
这里我们还可以传入多个泛型变量:
泛型类型(接口)
假设我们需要实现定义好一个类型的变量来接收一个泛型函数该怎么写:
或者通过通过对象(接口)类型的形式来定义:
这里我们就可以将其类型抽取为一个接口:
还有一种写法,下面这种写法的缺点是无法在定义类型的时候,直接定义好泛型是什么类型:
除了泛型接口,我们还可以创建泛型类。 注意,无法创建泛型枚举和泛型命名空间。
泛型类
泛型类看上去与泛型接口差不多。 泛型类使用(< >
)括起泛型类型,跟在类名后面。
先指定好类型变量,然后在类的内部就可以将其进行使用:
泛型约束
有些时候我们需要能够提前知道类型变量的一些信息。
比如下面的场景,我们需要获取参数的长度,但是这里会发现出现编译警告,这是因为TS
当前无法判断该泛型是否有length
属性:
因此这里我们需要限制一下传入的参数的类型,或者说限制一下泛型的类型,让其至少拥有length
属性。这里就需要用到extends
关键字:
对于这里的extends
可以理解为兼容,<A extends B>
可以看作让B
类型兼容A
类型,也就是A
类型的值能够赋值给B
类型。
具体关于兼容的概念可以查看 类型断言的限制章节 中关于关于类型兼容概念的总结。
在泛型约束中使用类型参数
你可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象obj
上,因此我们需要在这两个类型之间使用约束。
key of
的具体用法可以参考:keyof。
默认泛型参数
当一个泛型参数既没有显式指定,也没有推断出来的时候,默认泛型参数就会生效:
不只是函数上可以用,其他能够使用类型变量的地方基本都可以用:
枚举
使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例,比如下面:
枚举的成员会有对应的一个值,默认情况是以 0 开始, 以步长为 1 进行递增的数字。
枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:
我们这里所说的枚举是运行时存在的实体,在编译阶段不会擦除。
这里编译之后变成了:
这样看起来不太清楚,我们将其展开一下:
这样我们就知道了枚举类型值反向映射的原理了。
手动赋值
上文我们讲述的是枚举项自动默认赋值的情况,那这里我们如果要进行手动赋值呢, TS
支持数字的和基于字符串的赋值项。
手动赋值又被称之为初始化枚举项的表达式:
总结一下规律:在我们手动赋值(数值类型)时,以赋值为起点,以步长为 1 进行递增(不管起始点(赋值项)是小数,负数,或者后面的数字是否会重复)。
这里会发现通过赋的值来取对应的枚举值的时候, 丢失了前面的重复的值(被后面的覆盖了)。
因此在手动赋值的时候我们尽可能的避免枚举类型对应的值的重复, 这样会导致一些无法预估的错误。
常数项和计算所得项
首先我们要知道,什么在TS
的枚举中算得上是常数项和计算所得项呢?
当满足以下条件时,枚举成员被当作是常数项:
-
不具有初始化函数并且之前的枚举成员是常数项。
在这种情况下,当前枚举成员的值为上一个枚举成员的值加 1。
但第一个枚举元素是个例外。如果它没有初始化方法,那么它的初始值为 0。
-
枚举成员使用常数项枚举表达式初始化。常数项枚举表达式是
TS
表达式的子集,它可以在编译阶段求值。 当一个表达式满足下面条件之一时,它就是一个常数枚举表达式:-
数字字面量:
-
引用之前定义的常数项枚举成员(可以是在不同的枚举类型中定义的)如果这个成员是在同一个枚举类型中定义的,可以使用非限定名来引用:
-
带括号的常数项枚举表达式;
-
+, -, ~
一元运算符应用于常数项枚举表达式: -
+, -, *, /, %, <<, >>, >>>, &, |, ^
二元运算符,常数项枚举表达式做为其一个操作对象;
若常数项枚举表达式求值后为
NaN
或Infinity
,则会在编译阶段报错 -
然后除此之外所有其它情况的枚举成员被当作是需要计算得出的值(计算所得项)。
关于计算所得项在枚举中需要的注意点:如果紧接在计算所得项后面的是未手动赋值的项(紧接在手动赋值的后面的第一个即可),那么它就会因为无法获得初始值而报错。
前文说过,TS
支持数字的和基于字符串的赋值项。那么这里数字就能看作常数项,字符串就能看作计算所得项。
常量(const)枚举
前文所说的枚举不管是枚举项的定义或者枚举值在运行时会存在的,但是有时候,我们不希望产生过多的代码,但是又希望他能存在一定的实体。常量枚举就能满足这些条件,它的枚举项的定义在编译阶段会擦除,但是它的枚举值依旧能保留下来。
经过编译之后,枚举的定义部分将会被擦除,但是在 weeks
变量的使用中依旧保留下来了一些,值是枚举对应的值,注释部分对应的是枚举项:
我们再做一个测试:
编译之后
如果是普通的枚举项,我们来对比一下:
编译之后:
这里就能看的出来常量(const
)枚举和普通枚举的区别了。
外部枚举
外部枚举用来描述已经存在的枚举类型的形状,具体使用场景还不清楚。
外部枚举就更进一步,它只能再编译阶段生效,在运行时,关于枚举项的定义或者枚举值全部被擦除了。也就是说,外部枚举一定需要额外依赖我们定义的实体才能有用。
declare
声明的是类型变量,在编译阶段都会擦除,具体请参考:声明文件。
编译之后,关于枚举项和枚举值的定义直接擦除,如果没有额外定义一个实体,那么这里运行时会报错:
因此我们需要额外定义一个实体,这里可以结合 const
一起来使用:
编译后:
参考
声明文件(Coming Soon…)
当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
新语法索引
declare var
声明全局变量declare function
声明全局方法declare class
声明全局类declare enum
声明全局枚举类型declare namespace
声明(含有子属性的)全局对象interface
和type
声明全局类型export
导出变量export namespace
导出(含有子属性的)对象export default
ES6 默认导出export =
commonjs 导出模块export as namespace
UMD 库声明全局变量declare global
扩展全局变量declare module
扩展模块///
三斜线指令
这里额外补充一下
interface, type
,它们不需要declare
来显式表示,他们默认就是声明了类型;
这里要注意的是,涉及到 declare
声明的变量,它在编译之后会被擦除,因此他是需要依赖实体存在的。
声明类型,值,命名空间的关系
TS
中的声明会创建以下三种之一:命名空间,类型或值。 创建命名空间的声明会新建一个命名空间,它包含了用(.
)符号来访问时使用的名字。
Declaration Type | Namespace | Type | Value |
---|---|---|---|
Namespace | √ | √ | |
Class | √ | √ | |
Enum | √ | √ | |
Interface | √ | ||
Type Alias | √ | ||
Function | √ | ||
Variable | √ |
声明命名空间,值算做一类,声明类型(interface, type
)算作另外一类,它们的异同点:
相同点:
- 两者只是在编译期间存在,在编译成为
.js
之后就都被完全擦除了; - 他们都是帮助
TS
从另外一个维度(类型)的层面识别JS
实体;
不同点:
-
声明类型(
interface, type
)需要用声明的模型绑定到给定的名字上,也就是绑定到具体的JS
变量上: -
声明命名空间,值的声明会创建在
JS
输出中看到的值,也就是TS
直接通过这个声明来看JS
中的这个变量是什么,而不需要额外的绑定:这里要注意的是,因为
TS
同样回把声明的值看作变量,因此如果没有抽取到别的文件中,比如抽取到*.d.ts
中的话,会出现变量名冲突的情况:
声明语句
假设这里我们想应用jQuery
如果我们没有提供对应声明,那么在 TS
看来,jQuery
就无法被识别。
因此我们需要添加一个声明语句,在告诉TS
,这个变量是什么。
这里要注意的是,这里我们的declare var jQuery: (id: string) => any
并不是定义了一个实体的变量,它只是告诉了TS
这个jQuery
变量是什么。在编译之后,这个是会被擦除的。因此这里实际在运行时执行时,还是需要依赖真正的jQuery
变量。
上面的.ts
文件编译之后
这里如果直接执行就会报错,错误信息为:ReferenceError: jQuery is not defined
。
声明文件
TS
一般会检查项目目录下所有 *.ts
结尾的文件, 当然, 这里也包括了 *.d.ts
文件,而我们通常会把声明语句放到一个单独的文件(*.d.ts
)中,这就是声明文件。
这里要区分全局声明文件和局部声明文件,对于.ts
文件而言,有一个特殊的机制:
- 只要其中包含
export {}
或者其他可以代表其为模块的语句,那么*.ts
就为局部模块,那么所对应的*.d.ts
自然就是局部声明文件了。对于这种声明文件,我们需要额外的导入或者配置才能生效; - 反之,这个
*.d.ts
就是全局声明文件,我们不需要额外的配置,该项目下的.ts
文件就能识别到全局声明件中声明的类型了;
假如仍然无法解析,那么可以检查下 tsconfig.json
中的 files
、include
和 exclude
配置,确保我们定义的声明文件在TS
扫描范围之内。
比如这里我们写一个全局声明文件:
那么该项目下的另外一个文件就能获得该定义:
那么如何定义和使用一个局部声明文件呢,这个具体在后面讲。
第三方声明文件
当然,jQuery
的声明文件不需要我们定义了,社区已经帮我们定义好了:jQuery in DefinitelyTyped。
我们可以直接下载下来使用,但是更推荐的是使用 @types
统一管理第三方库的声明文件。
@types
的使用方式很简单,直接用 npm
安装对应的声明模块即可,以 jQuery
举例:
可以在这个页面搜索你需要的声明文件。
下载好的声明文件默认会在 node_modules/@types
目录下,这就说明,TS
默认情况下也会读取该目录。
书写声明文件
当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了。前面只介绍了最简单的声明文件内容,而真正书写一个声明文件并不是一件简单的事,以下会详细介绍如何书写声明文件。
在不同的场景下,声明文件的内容和使用方式会有所区别。
库的使用场景主要有以下几种:
- 全局变量:通过
<script>
标签引入第三方库,注入全局变量 - npm 包:通过
import foo from 'foo'
导入,符合 ES6 模块规范 - UMD 库:既可以通过
<script>
标签引入,又可以通过import
导入 - 直接扩展全局变量:通过
<script>
标签引入后,改变一个全局变量的结构 - 在 npm 包或 UMD 库中扩展全局变量:引用 npm 包或 UMD 库后,改变一个全局变量的结构
- 模块插件:通过
<script>
或import
导入后,改变另一个模块的结构
参考
声明合并
接口的合并
最简单也最常见的声明合并类型是接口合并。 从根本上说,合并的机制是把双方的成员放到一个同名的接口里。
如下所示:
合并接口其实也有一定的限制,对于合并的多个接口而言:
-
对于不是函数类型的属性而言,该属性应该是唯一的。 如果它们不是唯一的,那么它们必须是相同的类型。
如果两个接口中同时声明了同名的非函数成员且它们的类型不同,则编译器会报错:
-
对于是函数类型的属性而言,每个同名函数声明都会被当成这个函数的一个重载,同时需要注意,当两个接口合并时,后面的接口中的函数属性具有更高的优先级:
注意每组接口内部的声明顺序保持不变,但各组接口之间的顺序是后来的接口重载出现在靠前位置。
这个规则有一个例外是当出现特殊的函数签名时。
如果签名里有一个参数的类型是单一的字符串字面量(比如,不是字符串字面量的联合类型),那么它将会被提升到重载列表的最顶端。
比如,下面的接口会合并到一起:
合并后的
Document
将会像下面这样:看起来就像是按照函数参数的类型就像由具体到抽象来排序,对于类型相同的参数则是后面合并的接口优先级更高。
函数的合并
其实这里算不上声明的合并,因为这里定义的函数是实体,而非声明的值,类型,命名空间等。
这里应该算函数的重载:
其他声明合并
其实声明合并还有其他合并的形式,请参考:更多声明合并形式。
非法的合并
TypeScript 并非允许所有的合并。 目前,类不能与其它类或变量合并。 想要了解如何模仿类的合并,请参考 TypeScript 的混入。
工程化
tsconfig.json
概述
如果一个目录下存在一个tsconfig.json
文件,那么它意味着这个目录是 TypeScript 项目的根目录。 tsconfig.json
文件中指定了用来编译这个项目的根文件和编译选项。 一个项目可以通过以下方式之一来编译:
使用 tsconfig.json
- 不带任何输入文件的情况下调用
tsc
,编译器会从当前目录开始去查找tsconfig.json
文件,逐级向上搜索父目录; - 不带任何输入文件的情况下调用
tsc
,且使用命令行参数--project
(或-p
)指定一个包含tsconfig.json
文件的目录;
当命令行上指定了输入文件时,tsconfig.json
文件会被忽略,比如:tsc path/xxx.ts
。
示例
tsconfig.json
示例文件:
-
使用
"files"
属性: -
使用
"include"
和"exclude"
属性:
细节
"compilerOptions"
可以被忽略,这时编译器会使用默认值。在这里查看完整的 编译器选项 列表。
"files"
指定一个包含相对或绝对文件路径的列表。 "include"
和"exclude"
属性指定一个文件 glob 匹配模式列表。 支持的 glob 通配符有:
*
匹配 0 或多个字符(不包括目录分隔符)?
匹配一个任意字符(不包括目录分隔符)**/
递归匹配任意子目录
如果一个 glob 模式里的某部分只包含*
或.*
,那么仅有支持的文件扩展名类型被包含在内(比如默认.ts
,.tsx
,和.d.ts
, 如果allowJs
设置能true
还包含.js
和.jsx
)。
如果"files"
和"include"
都没有被指定,编译器默认包含当前目录和子目录下所有的 TypeScript
文件(.ts
, .d.ts
和 .tsx
),排除在"exclude"
里指定的文件。
JS 文件(.js
和.jsx
)也被包含进来,如果allowJs
被设置成true
。
如果指定了"files"
或"include"
,编译器会将它们结合一并包含进来。
使用"outDir"
指定的目录下的文件永远会被编译器排除,除非你明确地使用"files"
将其包含进来(这时就算用exclude
指定也没用)。
使用"include"
引入的文件可以使用"exclude"
属性过滤。
然而,通过"files"
属性明确指定的文件却总是会被包含在内,不管"exclude"
如何设置。
如果没有特殊指定,"exclude"
默认情况下会排除node_modules
,bower_components
,jspm_packages
和<outDir>
目录。
任何被"files"
或"include"
指定的文件所引用的文件也会被包含进来。 A.ts
引用了B.ts
,因此B.ts
不能被排除,除非引用它的A.ts
在"exclude"
列表中。
需要注意编译器不会去引入那些可能做为输出的文件;
比如,假设我们包含了index.ts
,那么index.d.ts
和index.js
会被排除在外。 通常来讲,不推荐只有扩展名的不同来区分同目录下的文件。
我们设置outDir
,让输出目录和可能会出现冲突的文件的目录不在同一目录是解决上面这个问题的一种办法。
tsconfig.json
文件可以是个空文件,那么所有默认的文件(如上面所述)都会以默认配置选项编译。
在命令行上指定的编译选项会覆盖在tsconfig.json
文件里的相应选项。
typeRoots 和 types
默认所有可见的”@types
”包会在编译过程中被包含进来。 node_modules/@types
文件夹下以及它们子文件夹下的所有包都是可见的。
也就是说,./node_modules/@types/
,../node_modules/@types/
和../../node_modules/@types/
等等。
而如果你手动设置了typeRoots
,那么只有typeRoots
下面的包才会被包含进来。 比如:
这个配置文件会包含所有./typings
下面的包,而不包含./node_modules/@types
里面的包。
如果指定了types
,只有被列出来的包才会被包含进来。 比如:
这个tsconfig.json
文件将仅会包含 ./node_modules/@types/node
,./node_modules/@types/lodash
和./node_modules/@types/express
。
node_modules/@types/*
里面的其它包不会被引入进来。
因此我们一般是指定"types": []
来禁用自动引入@types
包。
注意,自动引入只在你使用了全局的声明(相反于模块)时是重要的。 如果你使用import "foo"
语句,TypeScript
仍然会查找node_modules
和node_modules/@types
文件夹来获取foo
包。
使用 extends 继承配置
tsconfig.json
文件可以利用extends
属性从另一个配置文件里继承配置。
extends
是tsconfig.json
文件里的顶级属性(与compilerOptions
,files
,include
,和exclude
一样)。 extends
的值是一个字符串,包含指向另一个要继承文件的路径。
在原文件里的配置先被加载,然后被来自继承文件里的配置重写。 如果发现循环引用,则会报错。
来自所继承配置文件的files
,include
和exclude
覆盖源配置文件的属性。
配置文件里的相对路径在解析时相对于它所在的文件。
比如:
configs/base.json
:
tsconfig.json
:
tsconfig.nostrictnull.json
:
compileOnSave
在最顶层设置compileOnSave
标记,可以让 IDE
在保存文件的时候根据tsconfig.json
重新生成文件。
要想支持这个特性需要Visual Studio 2015, TypeScript1.8.4
以上并且安装atom-typescript插件。
compilerOptions(编译选项)
选项 | 类型 | 默认值 | 描述 |
---|---|---|---|
--allowJs | boolean | false | 允许编译 javascript 文件。 |
--allowSyntheticDefaultImports | boolean | module === "system" 或设置了--esModuleInterop | 允许从没有设置默认导出的模块中默认导入。这并不影响代码的输出,仅为了类型检查。 |
--allowUnreachableCode | boolean | false | 不报告执行不到的代码错误。 |
--allowUnusedLabels | boolean | false | 不报告未使用的标签错误。 |
--alwaysStrict | boolean | false | 以严格模式解析并为每个源文件生成"use strict" 语句 |
--baseUrl | string | 解析非相对模块名的基准目录。查看模块解析文档了解详情。 | |
--build -b | boolean | false | 使用Project References来构建此工程及其依赖工程。注意这个标记与本页内其它标记不兼容。详情参考这里 |
--charset | string | "utf8" | 输入文件的字符集。 |
--checkJs | boolean | false | 在.js 文件中报告错误。与--allowJs 配合使用。 |
--composite | boolean | true | 确保 TypeScript 能够找到编译当前工程所需要的引用工程的输出位置。 |
--declaration -d | boolean | false | 生成相应的.d.ts 文件。 |
--declarationDir | string | 生成声明文件的输出路径。 | |
--diagnostics | boolean | false | 显示诊断信息。 |
--disableSizeLimit | boolean | false | 禁用 JavaScript 工程体积大小的限制 |
--emitBOM | boolean | false | 在输出文件的开头加入 BOM 头(UTF-8 Byte Order Mark)。 |
--emitDecoratorMetadata [1] | boolean | false | 给源码里的装饰器声明加上设计类型元数据。查看issue #2577了解更多信息。 |
--experimentalDecorators [1] | boolean | false | 启用实验性的 ES 装饰器。 |
--extendedDiagnostics | boolean | false | 显示详细的诊段信息。 |
--forceConsistentCasingInFileNames | boolean | false | 禁止对同一个文件的不一致的引用。 |
--generateCpuProfile | string | profile.cpuprofile | 在指定目录生成 CPU 资源使用报告。若传入的是已创建的目录名,将在此目录下生成以时间戳命名的报告。 |
--help -h | 打印帮助信息。 | ||
--importHelpers | string | 从tslib 导入辅助工具函数(比如__extends ,__rest 等) | |
--importsNotUsedAsValues | string | remove | 用于设置针对于类型导入的代码生成和代码检查的行为。"remove" 和"preserve" 设置了是否对未使用的导入了模块副作用的导入语句生成相关代码,"error" 则强制要求只用作类型的模块导入必须使用import type 语句。 |
--inlineSourceMap | boolean | false | 生成单个 sourcemaps 文件,而不是将每 sourcemaps 生成不同的文件。 |
--inlineSources | boolean | false | 将代码与 sourcemaps 生成到一个文件中,要求同时设置了--inlineSourceMap 或--sourceMap 属性。 |
--init | 初始化 TypeScript 项目并创建一个tsconfig.json 文件。 | ||
--isolatedModules | boolean | false | 执行额外检查以确保单独编译(如transpileModule 或@babel/plugin-transform-typescript)是安全的。 |
--jsx | string | "preserve" | 在.tsx 文件里支持 JSX:"react" 或"preserve" 或"react-native" 。查看JSX。 |
--jsxFactory | string | "React.createElement" | 指定生成目标为 react JSX 时,使用的 JSX 工厂函数,比如React.createElement 或h 。 |
--lib | string[] | 编译过程中需要引入的库文件的列表。 可能的值为: ► ES5 ► ES6 ► ES2015 ► ES7 ► ES2016 ► ES2017 ► ES2018 ► ESNext ► DOM ► DOM.Iterable ► WebWorker ► ScriptHost ► ES2015.Core ► ES2015.Collection ► ES2015.Generator ► ES2015.Iterable ► ES2015.Promise ► ES2015.Proxy ► ES2015.Reflect ► ES2015.Symbol ► ES2015.Symbol.WellKnown ► ES2016.Array.Include ► ES2017.object ► ES2017.Intl ► ES2017.SharedMemory ► ES2017.String ► ES2017.TypedArrays ► ES2018.Intl ► ES2018.Promise ► ES2018.RegExp ► ESNext.AsyncIterable ► ESNext.Array ► ESNext.Intl ► ESNext.Symbol 注意:如果--lib 没有指定默认注入的库的列表。默认注入的库为: ► 针对于--target ES5 :DOM,ES5,ScriptHost ► 针对于--target ES6 :DOM,ES6,DOM.Iterable,ScriptHost | |
--listEmittedFiles | boolean | false | 打印出编译后生成文件的名字。 |
--listFiles | boolean | false | 编译过程中打印文件名。 |
--locale | string | (platform specific) | 显示错误信息时使用的语言,比如:en-us。 |
--mapRoot | string | 为调试器指定指定 sourcemap 文件的路径,而不是使用生成时的路径。当.map 文件是在运行时指定的,并不同于js 文件的地址时使用这个标记。指定的路径会嵌入到sourceMap 里告诉调试器到哪里去找它们。使用此标识并不会新创建指定目录并生成 map 文件在指定路径下。而是增加一个构建后的步骤,把相应文件移动到指定路径下。 | |
--maxNodeModuleJsDepth | number | 0 | node_modules 依赖的最大搜索深度并加载 JavaScript 文件。仅适用于--allowJs 。 |
--module -m | string | target === "ES6" ? "ES6" : "commonjs" | 指定生成哪个模块系统代码:"None" ,"CommonJS" ,"AMD" ,"System" ,"UMD" ,"ES6" 或"ES2015" 。 ► 只有"AMD" 和"System" 能和--outFile 一起使用。 ►"ES6" 和"ES2015" 可使用在目标输出为"ES5" 或更低的情况下。 |
--moduleResolution | string | module === "AMD" or "System" or "ES6" ? "Classic" : "Node" | 决定如何处理模块。或者是"Node" 对于 Node.js/io.js,或者是"Classic" (默认)。查看模块解析了解详情。 |
--newLine | string | (platform specific) | 当生成文件时指定行结束符:"crlf" (windows)或"lf" (unix)。 |
--noEmit | boolean | false | 不生成输出文件。 |
--noEmitHelpers | boolean | false | 不在输出文件中生成用户自定义的帮助函数代码,如__extends 。 |
--noEmitOnError | boolean | false | 报错时不生成输出文件。 |
--noErrorTruncation | boolean | false | 不截短错误消息。 |
--noFallthroughCasesInSwitch | boolean | false | 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿) |
--noImplicitAny | boolean | false | 在表达式和声明上有隐含的any 类型时报错。 |
--noImplicitReturns | boolean | false | 不是函数的所有返回路径都有返回值时报错。 |
--noImplicitThis | boolean | false | 当this 表达式的值为any 类型的时候,生成一个错误。 |
--noImplicitUseStrict | boolean | false | 模块输出中不包含"use strict" 指令。 |
--noLib | boolean | false | 不包含默认的库文件(lib.d.ts )。 |
--noResolve | boolean | false | 不把/// <reference``> 或模块导入的文件加到编译文件列表。 |
--noStrictGenericChecks | boolean | false | 禁用在函数类型里对泛型签名进行严格检查。 |
--noUnusedLocals | boolean | false | 若有未使用的局部变量则抛错。 |
--noUnusedParameters | boolean | false | 若有未使用的参数则抛错。 |
--out | string | 弃用。使用 --outFile 代替。 | |
--outDir | string | 重定向输出目录。 | |
--outFile | string | 将输出文件合并为一个文件。合并的顺序是根据传入编译器的文件顺序和///<reference``> 和import 的文件顺序决定的。查看输出文件顺序文档了解详情。 | |
paths [2] | Object | 模块名到基于baseUrl 的路径映射的列表。查看模块解析文档了解详情。 | |
--preserveConstEnums | boolean | false | 保留const 和enum 声明。查看const enums documentation了解详情。 |
--preserveSymlinks | boolean | false | 不把符号链接解析为其真实路径;将符号链接文件视为真正的文件。 |
--preserveWatchOutput | boolean | false | 保留 watch 模式下过时的控制台输出。 |
--pretty [1] | boolean | false | 给错误和消息设置样式,使用颜色和上下文。 |
--project -p | string | 编译指定目录下的项目。这个目录应该包含一个tsconfig.json 文件来管理编译。查看tsconfig.json文档了解更多信息。 | |
--reactNamespace | string | "React" | 当目标为生成"react" JSX 时,指定createElement 和__spread 的调用对象 |
--removeComments | boolean | false | 删除所有注释,除了以/!* 开头的版权信息。 |
--rootDir | string | (common root directory is computed from the list of input files) | 仅用来控制输出的目录结构--outDir 。 |
rootDirs [2] | string[] | 根(root)文件夹列表,表示运行时组合工程结构的内容。查看模块解析文档了解详情。 | |
--showConfig | boolean | false | 不真正执行 build,而是显示 build 使用的配置文件信息。 |
--skipDefaultLibCheck | boolean | false | 忽略库的默认声明文件的类型检查。 |
--skipLibCheck | boolean | false | 忽略所有的声明文件(*.d.ts )的类型检查。 |
--sourceMap | boolean | false | 生成相应的.map 文件。 |
--sourceRoot | string | 指定 TypeScript 源文件的路径,以便调试器定位。当 TypeScript 文件的位置是在运行时指定时使用此标记。路径信息会被加到sourceMap 里。 | |
--strict | boolean | false | 启用所有严格检查选项。 包含--noImplicitAny , --noImplicitThis , --alwaysStrict , --strictBindCallApply , --strictNullChecks , --strictFunctionTypes 和--strictPropertyInitialization . |
--strictFunctionTypes | boolean | false | 禁用函数参数双向协变检查。 |
--strictPropertyInitialization | boolean | false | 确保类的非undefined 属性已经在构造函数里初始化。若要令此选项生效,需要同时启用--strictNullChecks 。 |
--strictNullChecks | boolean | false | 在严格的null 检查模式下,null 和undefined 值不包含在任何类型里,只允许用它们自己和any 来赋值(有个例外,undefined 可以赋值到void )。 |
--suppressExcessPropertyErrors [1] | boolean | false | 阻止对对象字面量的额外属性检查。 |
--suppressImplicitAnyIndexErrors | boolean | false | 阻止--noImplicitAny 对缺少索引签名的索引对象报错。查看issue #1232了解详情。 |
--target -t | string | "ES3" | 指定 ECMAScript 目标版本"ES3" (默认),"ES5" ,"ES6" /"ES2015" ,"ES2016" ,"ES2017" ,"ES2018" ,"ES2019" ,"ES2020" 或"ESNext" 。 注意:"ESNext" 最新的生成目标列表为ES proposed features |
--traceResolution | boolean | false | 生成模块解析日志信息 |
--types | string[] | 要包含的类型声明文件名列表。查看@types,—typeRoots 和—types章节了解详细信息。 | |
--typeRoots | string[] | 要包含的类型声明文件路径列表。查看@types,—typeRoots 和—types章节了解详细信息。 | |
--version -v | 打印编译器版本号。 | ||
--watch -w | 在监视模式下运行编译器。会监视输出文件,在它们改变时重新编译。监视文件和目录的具体实现可以通过环境变量进行配置。详情请看配置 Watch。 |
- [1] 这些选项是试验性的。
- [2] 这些选项只能在
tsconfig.json
里使用,不能在命令行使用。
compilerOptions 中的 watch
编译器支持使用环境变量配置如何监视文件和目录的变化。
使用TSC_WATCHFILE
环境变量来配置文件监视
选项 | 描述 |
---|---|
PriorityPollingInterval | 使用fs.watchFile 但针对源码文件,配置文件和消失的文件使用不同的轮询间隔 |
DynamicPriorityPolling | 使用动态队列,对经常被修改的文件使用较短的轮询间隔,对未修改的文件使用较长的轮询间隔 |
UseFsEvents | 使用 fs.watch ,它使用文件系统事件(但在不同的系统上可能不一定准确)来查询文件的修改/创建/删除。注意少数的系统如 Linux,对监视者的数量有限制,如果使用fs.watch 创建监视失败那么将通过fs.watchFile 来创建监视 |
UseFsEventsWithFallbackDynamicPolling | 此选项与UseFsEvents 类似,只不过当使用fs.watch 创建监视失败后,回退到使用动态轮询队列进行监视(如DynamicPriorityPolling 介绍的那样) |
UseFsEventsOnParentDirectory | 此选项通过fs.watch (使用系统文件事件)监视文件的父目录,因此 CPU 占用率低但也会降低精度 |
默认 (无指定值) | 如果环境变量TSC_NONPOLLING_WATCHER 设置为true ,监视文件的父目录(如同UseFsEventsOnParentDirectory )。否则,使用fs.watchFile 监视文件,超时时间为250ms 。 |
使用TSC_WATCHDIRECTORY
环境变量来配置目录监视
在那些 Nodejs 原生就不支持递归监视目录的平台上,我们会根据TSC_WATCHDIRECTORY
的不同选项递归地创建对子目录的监视。 注意在那些原生就支持递归监视目录的平台上(如 Windows),这个环境变量会被忽略。
选项 | 描述 |
---|---|
RecursiveDirectoryUsingFsWatchFile | 使用fs.watchFile 监视目录和子目录,它是一个轮询监视(消耗 CPU 周期) |
RecursiveDirectoryUsingDynamicPriorityPolling | 使用动态轮询队列来获取目录与其子目录的改变 |
默认 (无指定值) | 使用fs.watch 来监视目录及其子目录 |
背景
在编译器中--watch
的实现依赖于 Nodejs 提供的fs.watch
和fs.watchFile
,两者各有优缺点。
fs.watch
使用文件系统事件通知文件及目录的变化。 但是它依赖于操作系统,且事件通知并不完全可靠,在很多操作系统上的行为难以预料。 还可能会有创建监视个数的限制,如 Linux 系统,在包含大量文件的程序中监视器个数很快被耗尽。 但也正是因为它使用文件系统事件,不需要占用过多的 CPU 周期。 典型地,编译器使用fs.watch
来监视目录(比如配置文件里声明的源码目录,无法进行模块解析的目录)。 这样就可以处理改动通知不准确的问题。 但递归地监视仅在 Windows 和 OSX 系统上支持。 这就意味着在其它系统上要使用替代方案。
fs.watchFile
使用轮询,因此涉及到 CPU 周期。 但是这是最可靠的获取文件/目录状态的机制。 典型地,编译器使用fs.watchFile
监视源文件,配置文件和消失的文件(失去文件引用),这意味着对 CPU 的使用依赖于程序里文件的数量。
代码质量检查和格式化
具体细节可以参考 工程化之代码规范。
概述
2019 年 1 月,TypeScirpt 官方决定全面采用 ESLint 作为代码检查的工具,并创建了一个新项目 typescript-eslint,提供了 TypeScript
文件的解析器 @typescript-eslint/parser 和相关的配置选项 @typescript-eslint/eslint-plugin 等。而之前的两个 lint
解决方案都将弃用:
- typescript-eslint-parser 已停止维护;
- TSLint 将提供迁移工具,并在
typescript-eslint
的功能足够完整后停止维护 TSLint);
综上所述,目前以及将来的 TypeScript
的代码检查方案就是 typescript-eslint。
为什么需要代码检查
有人会觉得,JavaScript
非常灵活,所以需要代码检查。而 TypeScript
已经能够在编译阶段检查出很多问题了,为什么还需要代码检查呢?
因为 TypeScript
关注的重心是类型的检查,而不是代码风格。当团队的人员越来越多时,同样的逻辑不同的人写出来可能会有很大的区别:
- 缩进应该是四个空格还是两个空格?
- 是否应该禁用
var
? - 接口名是否应该以
I
开头? - 是否应该强制使用
===
而不是==
?
这些问题 TypeScript
不会关注,但是却影响到多人协作开发时的效率、代码的可理解性以及可维护性。
下面来看一个具体的例子:
以上代码你能看出有什么错误吗?
分别用 tsc 编译和 eslint 检查后,报错信息如下:
存在的问题 | tsc 是否报错 | eslint 是否报错 |
---|---|---|
应该使用 let 或 const 而不是 var | ❌ | ✅ |
myName 被误写成了 myNane | ✅ | ✅ |
toString 被误写成了 toStrng | ✅️ | ❌ |
上例中,我们使用了 var
来定义一个变量,但其实 ES6 中有更先进的语法 let
和 const
,此时就可以通过 eslint
检查出来,提示我们应该使用 let
或 const
而不是 var
。
对于未定义的变量 myNane
,tsc
和 eslint
都可以检查出来。
由于 eslint
无法识别 myName
存在哪些方法,所以对于拼写错误的 toString
没有检查出来。
由此可见,eslint
能够发现出一些 tsc
不会关心的错误,检查出一些潜在的问题,所以代码检查还是非常重要的。
在 TypeScript 中使用 ESLint
安装 ESLint
ESLint 可以安装在当前项目中或全局环境下,因为代码检查是项目的重要组成部分,所以我们一般会将它安装在当前项目中。可以运行下面的脚本来安装:
由于 ESLint 默认使用 Espree 进行语法解析,无法识别 TypeScript
的一些语法,故我们需要安装 @typescript-eslint/parser
,替代掉默认的解析器,别忘了同时安装 typescript
:
接下来需要安装对应的插件 @typescript-eslint/eslint-plugin 它作为 eslint
默认规则的补充,提供了一些额外的适用于 ts
语法的规则。
配置ESLint
我们在项目的根目录下创建一个 .eslintrc.js
,内容如下:
执行 ESLint
这里我们可以执行:
在 TypeScript 中使用 Prettier
ESLint
包含了一些代码格式的检查,比如空格、分号等。但前端社区中有一个更先进的工具可以用来格式化代码,那就是 Prettier。
Prettier
聚焦于代码的格式化,通过语法分析,重新整理代码的格式,让所有人的代码都保持同样的风格。
安装 Prettier
首先需要安装 Prettier:
配置 Prettier
然后创建一个 .prettierrc.js
文件,里面包含 Prettier
的配置项。
具体哪些配置项可以参考:Prettier 文档 - 配置项
执行 Prettier
整合 prettier + eslint + vscode
参考
- TS Handbook 中文版 - tsconfig 配置
- TS Handbook 中文版 - 编译选项
- TS Handbook 中文版 - 配置 watch
- TS 入门教程 - 代码检查
- typescript-eslint 官方文档
- ESLint 文档 - 中文版
- Prettier 文档 - 配置
混合开发中的 JS 代码
在TS
中我们可以和JS
一起混合开发,不过这需要我们在编译选项中配置,"allowJs": true
,这个时候,TS
会将JS
也当作输入文件(将其解析为TS
),然后输出依旧是JS
,输入输出都是JS
文件,只不过加了一个转换为TS
的中间过程。
默认情况下,TS
为了提高转换速度,只会对JS
进行输入输出,而不会进行类型校验和错误提示,这个时候与allowJS
对应的配置checkJs
就有用了。
TypeScript 2.3
以后的版本支持使用--checkJs
对.js
文件进行类型检查和错误提示。
你可以通过添加// @ts-nocheck
注释来忽略(整个文件)类型检查;你还可以使用// @ts-ignore
来忽略本行的错误。
相反,你可以通过去掉--checkJs
设置并添加一个// @ts-check
注释来选择检查某些.js
文件。
如果你使用了tsconfig.json
,JS
检查将遵照一些严格检查标记,如noImplicitAny
,strictNullChecks
等。
但因为JS
检查是相对宽松的,在使用严格标记时可能会有些出乎意料的情况。
对比.js
文件和.ts
文件在类型检查上的差异,有如下几点需要注意:
用 JSDoc 类型表示类型信息
.js
文件里,类型可以和在.ts
文件里一样被推断出来。 同样地,当类型不能被推断时,它们可以通过 JSDoc 来指定,就好比在.ts
文件里那样。 如同 TypeScript,--noImplicitAny
会在编译器无法推断类型的位置报错。 (除了对象字面量的情况;后面会详细介绍)
JSDoc
注解修饰的声明会被设置为这个声明的类型。比如:
你可以在这里找到所有 JSDoc
支持的模式,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
文件里添加类型信息。
注意,没有在下面列出的标记(例如@async
)都是还不支持的。
@type
@param
(or@arg
or@argument
)@returns
(or@return
)@typedef
@callback
@template
@class
(or@constructor
)@this
@extends
(or@augments
)@enum
它们代表的意义与 usejsdoc.org
上面给出的通常是一致的或者是它的超集。 下面的代码描述了它们的区别并给出了一些示例。
@type
可以使用@type
标记并引用一个类型名称(原始类型,TypeScript
里声明的类型,或在 JSDoc
里@typedef
标记指定的) 可以使用任何 TypeScript
类型和大多数 JSDoc
类型。
@type
可以指定联合类型—例如,string
和boolean
类型的联合。
注意,括号是可选的。
有多种方式来指定数组类型:
还可以指定对象字面量类型。 例如,一个带有a
(字符串)和b
(数字)属性的对象,使用下面的语法:
可以使用字符串和数字索引签名来指定map-like
和array-like
的对象,使用标准的 JSDoc
语法或者 TypeScript
语法。
这两个类型与 TypeScript
里的{ [x: string]: number }
和{ [x: number]: any }
是等同的。编译器能识别出这两种语法。
可以使用 TypeScript
或 Closure
语法指定函数类型。
或者直接使用未指定的Function
类型:
Closure
的其它类型也可以使用:
转换
TypeScript
借鉴了 Closure
里的转换语法。 在括号表达式前面使用@type
标记,可以将一种类型转换成另一种类型
导入类型
可以使用导入类型从其它文件中导入声明。 这个语法是 TypeScript
特有的,与 JSDoc
标准不同:
导入类型也可以使用在类型别名声明中:
导入类型可以用在从模块中得到一个值的类型。
@param
和@returns
@param
语法和@type
相同,但增加了一个参数名。 使用[]
可以把参数声明为可选的:
函数的返回值类型也是类似的:
@typedef
, @callback
, 和 @param
@typedef
可以用来声明复杂类型。 和@param
类似的语法。
可以在第一行上使用object
或Object
。
@param
允许使用相似的语法。 注意,嵌套的属性名必须使用参数名做为前缀:
@callback
与@typedef
相似,但它指定函数类型而不是对象类型:
当然,所有这些类型都可以使用 TypeScript
的语法@typedef
在一行上声明:
@template
使用@template
声明泛型:
用逗号或多个标记来声明多个类型参数:
还可以在参数名前指定类型约束。 只有列表的第一项类型参数会被约束:
@constructor
编译器通过this
属性的赋值来推断构造函数,但你可以让检查更严格提示更友好,你可以添加一个@constructor
标记:
通过@constructor
,this
将在构造函数C
里被检查,因此你在initialize
方法里得到一个提示,如果你传入一个数字你还将得到一个错误提示。如果你直接调用C
而不是构造它,也会得到一个错误。
不幸的是,这意味着那些既能构造也能直接调用的构造函数不能使用@constructor
。
@this
编译器通常可以通过上下文来推断出this
的类型。但你可以使用@this
来明确指定它的类型:
@extends
当 JavaScript
类继承了一个基类,无处指定类型参数的类型。而@extends
标记提供了这样一种方式:
注意@extends
只作用于类。当前,无法实现构造函数继承类的情况。
@enum
@enum
标记允许你创建一个对象字面量,它的成员都有确定的类型。不同于 JavaScript
里大多数的对象字面量,它不允许添加额外成员。
注意@enum
与 TypeScript
的@enum
大不相同,它更加简单。然而,不同于 TypeScript
的枚举,@enum
可以是任何类型:
更多示例
已知不支持的模式
在值空间中将对象视为类型是不可以的,除非对象创建了类型,如构造函数。
对象字面量属性上的=
后缀不能指定这个属性是可选的:
Nullable
类型只在启用了strictNullChecks
检查时才启作用:
Non-nullable
类型没有意义,以其原类型对待:
不同于 JSDoc
类型系统,TypeScript
只允许将类型标记为包不包含null
。 如果启用了strictNullChecks
,那么number
是非null
的。 如果没有启用,那么number
是可以为null
的。