自我介绍:大家好,我是吉帅振的网络日志(其他平台账号名字相同),互联网前端开发工程师,工作5年,去过上海和北京,经历创业公司,加入过阿里本地生活团队,现在郑州北游教育从事编程培训。
一、前言
{
letstr:string=thisisstring;
letnum:number=1;
letbool:boolean=true;
}
{
conststr:string=thisisstring;
constnum:number=1;
constbool:boolean=true;
}
在示例中,使用let定义变量时,我们写明类型注解也就罢了,毕竟值可能会被改变。可是,使用const常量时还需要写明类型注解,那可真的很麻烦。实际上,TypeScript早就考虑到了这么简单而明显的问题。在很多情况下,TypeScript会根据上下文环境自动推断出变量的类型,无须我们再写明类型注解。因此,上面的示例可以简化为如下所示内容:
{
letstr=thisisstring;//等价
letnum=1;//等价
letbool=true;//等价
}
{
conststr=thisisstring;//不等价
constnum=1;//不等价
constbool=true;//不等价
}
二、类型推断
在TypeScript中,类型标注声明是在变量之后(即类型后置),它不像Java语言一样,先声明变量的类型,再声明变量的名称。使用类型标注后置的好处是编译器可以通过代码所在的上下文推导其对应的类型,无须再声明变量类型,具体示例如下:
{
letx1=42;//推断出x1的类型是number
letx2:number=x1;//ok
}
在上述代码中,x1的类型被推断为number,将变量赋值给number类型的变量x2后,不会出现任何错误。在TypeScript中,具有初始化值的变量、有默认值的函数参数、函数返回的类型(05讲中会专门介绍函数类型)都可以根据上下文推断出来。比如我们能根据return语句推断函数返回的类型,如下代码所示:
{
/**根据参数的类型,推断出返回值的类型也是number*/
functionadd1(a:number,b:number){
returna+b;
}
constx1=add1(1,1);//推断出x1的类型也是number
/**推断参数b的类型是数字或者undefined,返回值的类型也是数字*/
functionadd2(a:number,b=1){
returna+b;
}
constx2=add2(1);
constx3=add2(1,1);//ts()Argumentoftype"1"isnotassignabletoparameteroftypenumber
undefined
}
在上述add1函数中,我们return了变量a+b的结果,因为a和b的类型为number,所以函数返回类型被推断为number。当然,拥有默认值的函数参数的类型也能被推断出来。比如上述add2函数中,b参数被推断为number
undefined类型,如果我们给b参数传入一个字符串类型的值,由于函数参数类型不一致,此时编译器就会抛出一个ts()错误。
三、上下文推断
通过类型推断的例子,我们发现变量的类型可以通过被赋值的值进行推断。除此之外,在某些特定的情况下,我们也可以通过变量所在的上下文环境推断变量的类型,具体示例如下:
{
typeAdder=(a:number,b:number)=number;
constadd:Adder=(a,b)={
returna+b;
}
constx1=add(1,1);//推断出x1类型是number
constx2=add(1,1);//ts()Argumentoftype"1"isnotassignabletoparameteroftypenumber
}
这里我们定义了一个实现加法功能的函数类型Adder(定义的Adder类型使用了type类型别名,这点会在07讲专门介绍),声明了add变量的类型为Adder并赋值一个匿名箭头函数,箭头函数参数a和b的类型和返回类型都没有显式声明。TypeScript通过add的类型Adder反向(通过变量类型推断出值的相关类型)推断出箭头函数参数及返回值的类型,也就是说函数参数a、b,以及返回类型在这个变量的声明上下文中被确定了。正是得益于TypeScript这种类型推导机制和能力,使得我们无须显式声明,即可直接通过上下文环境推断出变量的类型,也就是说此时类型可缺省。下面回头看最前面的示例(如下所示),我们发现这些缺省类型注解的变量还可以通过类型推断出类型。
{
letstr=thisisstring;//str:string
letnum=1;//num:number
letbool=true;//bool:boolean
}
{
conststr=thisisstring;//str:thisisstring
constnum=1;//num:1
constbool=true;//bool:true
}
如上述代码中注释说明,通过let和const定义的赋予了相同值的变量,其推断出来的类型不一样。比如同样是thisisstring(这里表示一个字符串值),通过let定义的变量类型是string,而通过const定义的变量类型是thisisstring(这里表示一个字符串字面量类型)。这里我们可以通过VSCodehover示例中的变量查看类型,验证一下这个结论。
四、字面量类型
在TypeScript中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。目前,TypeScript支持3种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型,对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型,具体示例如下:
{
letspecifiedStr:thisisstring=thisisstring;
letspecifiedNum:1=1;
letspecifiedBoolean:true=true;
}
字面量类型是集合类型的子类型,它是集合类型的一种更具体的表达。比如thisisstring(这里表示一个字符串字面量类型)类型是string类型(确切地说是string类型的子类型),而string类型不一定是thisisstring(这里表示一个字符串字面量类型)类型,如下具体示例:
{
letspecifiedStr:thisisstring=thisisstring;
letstr:string=anystring;
specifiedStr=str;//ts()类型"string"不能赋值给类型thisisstring
str=specifiedStr;//ok
}
这里,我们通过一个更通俗的说法来理解字面量类型和所属集合类型的关系。比如说我们用“马”比喻string类型,即“黑马”代指thisisstring类型,“黑马”肯定是“马”,但“马”不一定是“黑马”,它可能还是“白马”“灰马”。因此,thisisstring字面量类型可以给string类型赋值,但是string类型不能给thisisstring字面量类型赋值,这个比喻同样适合于形容数字、布尔等其他字面量和它们父类的关系。接下来,我们介绍一下字符串字面量类型、数字字面量类型、布尔字面量类型。
五、字符串字面量类型
一般来说,我们可以使用一个字符串字面量类型作为变量的类型,如下代码所示:
lethello:hello=hello;
hello=hi;//ts()Type"hi"isnotassignabletotype"hello"
实际上,定义单个的字面量类型并没有太大的用处,它真正的应用场景是可以把多个字面量类型组合成一个联合类型,用来描述拥有明确成员的实用的集合。如下代码所示,我们使用字面量联合类型描述了一个明确、可up可down的集合,这样就能清楚地知道需要的数据结构了。
typeDirection=up
down;
functionmove(dir:Direction){
//...
}
move(up);//ok
move(right);//ts()Argumentoftype"right"isnotassignabletoparameteroftypeDirection
通过使用字面量类型组合的联合类型,我们可以限制函数的参数为指定的字面量类型集合,然后编译器会检查参数是否是指定的字面量类型集合里的成员。因此,相较于使用string类型,使用字面量类型(组合的联合类型)可以将函数的参数限定为更具体的类型。这不仅提升了程序的可读性,还保证了函数的参数类型,可谓一举两得。
六、数字字面量类型及布尔字面量类型
数字字面量类型和布尔字面量类型的使用与字符串字面量类型的使用类似,我们可以使用字面量组合的联合类型将函数的参数限定为更具体的类型,比如声明如下所示的一个类型Config:
interfaceConfig{
size:small
big;
isEnable:true
false;
margin:0
2
4;
}
在上述代码中,我们限定了size属性为字符串字面量类型small
big,isEnable属性为布尔字面量类型true
false(布尔字面量只包含true和false,true
false的组合跟直接使用boolean没有区别),margin属性为数字字面量类型0
2
4。介绍完三种字面量类型后,我们再来看看通过let和const定义的变量的值相同,而变量类型不一致的具体原因。我们先来看一个const示例,如下代码所示:
{
conststr=thisisstring;//str:thisisstring
constnum=1;//num:1
constbool=true;//bool:true
}
在上述代码中,我们将const定义为一个不可变更的常量,在缺省类型注解的情况下,TypeScript推断出它的类型直接由赋值字面量的类型决定,这也是一种比较合理的设计。接下来我们看看如下所示的let示例,此时理解起来可能会稍微难一些。
{
letstr=thisisstring;//str:string
letnum=1;//num:number
letbool=true;//bool:boolean
}
在上述代码中,缺省显式类型注解的可变更的变量的类型转换为了赋值字面量类型的父类型,比如str的类型是thisisstring类型(这里表示一个字符串字面量类型)的父类型string,num的类型是1类型的父类型number。这种设计符合编程预期,意味着我们可以分别赋予str和num任意值(只要类型是string和number的子集的变量):
str=anystring;
num=2;
bool=false;
我们将TypeScript的字面量子类型转换为父类型的这种设计称之为"literalwidening",也就是字面量类型的拓宽,比如上面示例中提到的字符串字面量类型转换成string类型,下面我们着重介绍一下。
七、LiteralWidening
所有通过let或var定义的变量、函数的形参、对象的非只读属性,如果满足指定了初始值且未显式添加类型注解的条件,那么它们推断出来的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽。下面我们通过字符串字面量的示例来理解一下字面量类型拓宽:
{
letstr=thisisstring;//类型是string
letstrFun=(str=thisisstring)=str;//类型是(str?:string)=string;
constspecifiedStr=thisisstring;//类型是thisisstring
letstr2=specifiedStr;//类型是string
letstrFun2=(str=specifiedStr)=str;//类型是(str?:string)=string;
}
因为第2-3行满足了let、形参且未显式声明类型注解的条件,所以变量、形参的类型拓宽为string(形参类型确切地讲是string
undefined)。因为第5行的常量不可变更,类型没有拓宽,所以specifiedStr的类型是thisisstring字面量类型。第7~8行,因为赋予的值specifiedStr的类型是字面量类型,且没有显式类型注解,所以变量、形参的类型也被拓宽了。其实,这样的设计符合实际编程诉求。我们设想一下,如果str2的类型被推断为thisisstring,它将不可变更,因为赋予任何其他的字符串类型的值都会提示类型错误。基于字面量类型拓宽的条件,我们可以通过如下所示代码添加显示类型注解控制类型拓宽行为。
{
constspecifiedStr:thisisstring=thisisstring;//类型是"thisisstring"
letstr2=specifiedStr;//即便使用let定义,类型是thisisstring
}
实际上,除了字面量类型拓宽之外,TypeScript对某些特定类型值也有类似"TypeWidening"(类型拓宽)的设计,下面我们具体来了解一下。
八、TypeWidening
比如对null和undefined的类型进行拓宽,通过let、var定义的变量如果满足未显式声明类型注解且被赋予了null或undefined值,则推断出这些变量的类型是any:
{
letx=null;//类型拓宽成any
lety=undefined;//类型拓宽成any
/**-----分界线-------*/
constz=null;//类型是null
/**-----分界线-------*/
letanyFun=(param=null)=param;//形参类型是null
letz2=z;//类型是null
letx2=x;//类型是null
lety2=y;//类型是undefined
}
注意:在严格模式下,一些比较老的版本中(2.0)null和undefined并不会被拓宽成“any”。因此,某些过时的资料中会存在与课程不一致的解释。在现代TypeScript中,以上示例的第23行的类型拓宽更符合实际编程习惯,我们可以赋予任何其他类型的值给具有null或undefined初始值的变量x和y。示例第行的类型推断行为因为开启了strictNullChecks=true(说明:本课程所有示例都基于严格模式编写),此时我们可以从类型安全的角度试着思考一下:这几行代码中出现的变量、形参的类型为什么是null或undefined,而不是any?因为前者可以让我们更谨慎对待这些变量、形参,而后者不能。既然有类型拓宽,自然也会有类型缩小,下面我们简单介绍一下TypeNarrowing。
九、TypeNarrowing
在TypeScript中,我们可以通过某些操作将变量的类型由一个较为宽泛的集合缩小到相对较小、较明确的集合,这就是"TypeNarrowing"。比如,我们可以使用类型守卫(详见11讲的内容)将函数参数的类型从any缩小到明确的类型,具体示例如下:
{
letfunc=(anything:any)={
if(typeofanything===string){
returnanything;//类型是string
}elseif(typeofanything===number){
returnanything;//类型是number
}
returnnull;
};
}
在VSCode中hover到第4行的anything变量提示类型是string,到第6行则提示类型是number。同样,我们可以使用类型守卫将联合类型(详见08讲内容)缩小到明确的子类型,具体示例如下:
{
letfunc=(anything:string
number)={
if(typeofanything===string){
returnanything;//类型是string
}else{
returnanything;//类型是number
}
};
}
当然,我们也可以通过字面量类型等值判断(===)或其他控制流语句(包括但不限于if、三目运算符、switch分支)将联合类型收敛为更具体的类型,如下代码所示:
{
typeGoods=pen
pencil
ruler;
constgetPenCost=(item:pen)=2;
constgetPencilCost=(item:pencil)=4;
constgetRulerCost=(item:ruler)=6;
constgetCost=(item:Goods)={
if(item===pen){
returngetPenCost(item);//item=pen
}elseif(item===pencil){
returngetPencilCost(item);//item=pencil
}else{
returngetRulerCost(item);//item=ruler
}
}
}
在上述getCost函数中,接受的参数类型是字面量类型的联合类型,函数内包含了if语句的3个流程分支,其中每个流程分支调用的函数的参数都是具体独立的字面量类型。那为什么类型由多个字面量组成的变量item可以传值给仅接收单一特定字面量类型的函数getPenCost、getPencilCost、getRulerCost呢?这是因为在每个流程分支中,编译器知道流程分支中的item类型是什么。比如item===pencil的分支,item的类型就被收缩为“pencil”。事实上,如果我们将上面的示例去掉中间的流程分支,编译器也可以推断出收敛后的类型,如下代码所示:
constgetCost=(item:Goods)={
if(item===pen){
item;//item=pen
}else{
item;//=pencil
ruler
}
}
十、总结
类型推断、字面量类型、类型拓宽、类型缩小等知识,涉及的都是比较简单的字面量、赋值、函数的编程场景,而这些知识同样适用于更复杂的类型和结构,你需要多花时间学习、理解并掌握。