当前位置: 首页 > 科技观察

一文读懂TypeScript类型兼容性_0

时间:2023-03-13 22:17:57 科技观察

大家好,我是CUGGZ。JavaScript是一种弱类型语言,它对类型执行弱检查。也正是因为这个特性,TypeScript这种强类型语言系统的出现弥补了类型检查的不足。TypeScript在实现强类型验证的同时,也需要满足JavaScript灵活的特性,所以就有了类型兼容的概念。了解类型兼容性可以避免实际开发中的一些低级错误。我们先来看看类型兼容的概念和分类。1.类型兼容性的概念所谓类型兼容性就是用来判断一个类型是否可以赋值给其他类型。TypeScript中的类型兼容性基于结构类型,这是一种仅使用其成员描述类型的方式。基本原则是,如果x要与y兼容,则y必须至少具有与x相同的属性。我们看一个例子,建一个Teacher类,然后声明一个接口Student,它有Student的所有属性,还有其他属性。在这种情况下,Student与Teacher兼容:classTeacher{constructor(publicweight:number,publicname:string,publicjob:string){}}interfaceStudent{name:stringweight:number}letx:Student;x=newTeacher(120,'TS','teacher')//?如果反过来,Teacher和Student不兼容,因为Student比Person少了一个属性。2.特殊类型的类型兼容性我们先来看看TypeScript中一些特殊类型的类型兼容性。(1)anyany类型可以赋给never以外的任何其他类型,反过来其他类型也可以赋给any。也就是说any可以兼容除never以外的所有类型,也可以兼容所有类型。letany:any;leta:number=any;//?设b:{}=any;//?设b:()=>number=any;//?(2)nevernever类型可以赋值给任何其他类型,但不能被任何其他类型赋值。让永不:从不=(()=>{抛出错误('从不');})();让一个:数字=从不;//?设b:()=>number=never;//?让c:{}=从不;//?如你所见,将never类型赋值给number、function或object类型都没有问题。(3)unknownunknown和never的特性是相反的,即unknown不能赋给any之外的任何其他类型,但是其他类型可以赋给unknown。让未知:未知;consta:number=未知;//类型“unknown”不能分配给类型“number”。constb:()=>数字=未知;//类型“unknown”不可分配给类型“()=>number”。constc:{}=未知;//类型“未知”不可分配给类型“{}”。可以看到,当给number、function、object类型赋值未知类型时,都会报错,这是因为类型不兼容。3.函数类型的类型兼容性函数的类型兼容性主要包括以下六个方面:(1)参数个数要与函数参数个数兼容,需要满足一个要求:如果函数y被赋值x,那么每个参数在y中应该有一个对应,即x的参数个数小于等于y的参数个数:设x=(a:number)=>0;让y=(b:数字,c:字符串)=>0;如果上面定义的两个函数都赋值了,看下面两种情况的结果:y=x;//?可以将x赋值给y,因为x的参数个数小于等于y的参数个数,而且参数名是否相同无所谓。将y赋给x是不可能的:x=y;//无法将类型“(b:number,c:string)=>number”分配给类型“(a:number)=>number”。这里y的参数个数比x的多,所以报错。(2)除了参数个数,函数的参数类型也需要对应:letx=(a:number)=>0;让y=(b:字符串)=>0;x=y;//错误不能是Type'(b:string)=>number'isassignedtotype'(a:number)=>number'。可以看出,x和y这两个函数的参数个数和返回值是一样的,但是参数类型不匹配,所以不行。(3)剩余参数和可选参数当要赋值的函数参数包含剩余参数(...args)时,赋值的函数可以替换为任意数量的参数,但类型需要对应:constgetNum=(arr:number[],callback:(...args:number[])=>number):number=>{returncallback(...arr);};getNum([1,2],(...args:number[]):number=>args.length//返回参数个数);剩下的参数其实可以看作是无数个可选参数,所以在兼容性上都是差不多的。下面看一个可选参数和剩余参数组合的例子://第二个参数callback是一个函数,函数的第二个参数是一个可选参数constgetNum=(arr:number[],callback:(arg1:number,arg2?:number)=>number):number=>{returncallback(...arr);//error应该有1-2个参数,但是获取的数大于等于0};这里因为arr可能是空数组,如果是空数组...arr不会传递任何实参给callback,所以这里会报错。如果换成returncallback(arr[0],...arr),就没有问题了。(4)参数的双向协变函数参数的双向协变是指参数类型不需要绝对相同:letfuncA=function(arg:number|string):void{};letfuncB=function(arg:number):void{};//funcA=funcB和funcB=funcA都可用。funcA和funcB的参数类型并不完全相同。funcA的参数类型为联合类型number|string,funcB的参数类型为number|string中的数字。这两个函数也是兼容的。(5)返回值类型函数的返回值类型也要对应:letx=(a:number):string|数字=>0;让y=(b:数字)=>"a";让z=(c:数字)=>false;x=y;//?x=z;//这里不能将type"(c:number)=>boolean"赋值给type"(a:number)=>string|number"x函数的返回值是一个联合类型,可以是字符串类型也可以是一个数字类型。y的返回值类型是number类型,参数的个数和类型都没有问题,所以可以赋值给x。而z的返回值类型false既不是string也不是number,所以无法赋值。(6)函数重载对于重载函数,要求每次重载赋值函数,都能在赋值所用的函数上找到对应的签名:functionmerge(arg1:number,arg2:number):number;//部分合并函数重载functionmerge(arg1:string,arg2:string):string;//部分合并函数重载functionmerge(arg1:any,arg2:any){//合并函数实体returnarg1+arg2;}functionsum(arg1:number,arg2:number):number;//重载求和函数的一部分functionsum(arg1:any,arg2:any):any{//求和函数实体returnarg1+arg2;}让func=合并;功能=总和;//错误无法将类型“(arg1:number,arg2:number)=>number”分配给类型“{(arg1:number,arg2:number):number;(arg1:string,arg2:string):string;}”sum函数的重载缺少参数,返回值为string,与merge函数不兼容,所以赋值时会报错4.枚举的类型兼容性Numeric枚举成员类型和数值类型相互兼容other:enumStatus{On,Off}lets=Status.On;s=1;s=3;虽然Status.On的值为0,但是因为数值枚举成员类型和数值类型是相互兼容的,这里给s赋3是没有问题的。但是不同的enum值是不兼容的:enumStatus{On,Off}enumColor{White,Black}lets=Status.On;s=Color.White;//不能assigntype"Color.White"赋予类型"Status",虽然Status.On和Color.White的值都是0,但是它们是不兼容的。字符串枚举成员类型和字符串类型不兼容:enumStatus{On='on',Off='off'}lets=Status.Ons='TypeScript'//Cannotassigntype"TypeScript"toType"Status"会报此处出现错误,因为字符串文字类型'TypeScript'和Status.On不兼容。5.类类型的类型兼容性在比较两个类的类型兼容性时,只会比较实例成员和方法,不会比较类的静态成员和构造函数:classAnimal{staticage:number;构造函数(公共名称:字符串){}}类人{静态年龄:字符串;constructor(publicname:string){}}classFood{constructor(publicname:number){}}leta:Animal;letp:People;letf:Food;a=p;//好的=f;//类型'Food'不能分配给类型'Animal'。Animal类和People类都有一个age静态属性,都定义了实例属性名,类型都是string。将People类型的p赋给Animal类型的a是没有问题的,因为比较类类型兼容性时,只比较实例的成员。两个变量虽然属于不同的类类型,但是它们都有相同的字段和类型的实例属性名,而且类的静态成员不影响兼容性,所以是兼容的。Food类定义了一个实例属性名,其类型为number,所以Food类型的f与Animal类型的a不兼容,不能赋值。类的私有成员和受保护成员:类的私有成员和受保护成员会影响类的兼容性。检查类的实例兼容性时,如果目标(被赋值)类型(这里的实例类型是创建它的类)包含私有成员,则源(被赋值)类型必须包含私有成员来自同一个类的私有成员,它允许子类赋值给父类:classParent{privateage:number;constructor(){}}classChildrenextendsParent{构造函数(){super();}}classOther{privateage:number;构造函数(){}}constchildren:Parent=newChildren();constother:Parent=newOther();//类型“Other”不可分配给类型“Parent”。该类型具有私有属性“age”的单独声明。当指定other为Parent类类型,将Other创建的实例赋值给other时,会报错。因为Parent的age属性是private成员,外部无法访问,所以类型不兼容。我们将孩子的类型指定为Parent类的类型,然后为其分配一个Children类的实例。没有问题,因为Children类继承了Parent类,实例属性没有区别。Parent类有一个私有属性age,但是因为Children类继承了Parent类,所以可以赋值。同样,用protected修饰符修饰的属性也是一样的:classParent{protectedage:number;constructor(){}}classChildrenextendsParent{构造函数(){super();}}classOther{protectedage:number;构造函数(){}}constchildren:Parent=newChildren();constother:Parent=newOther();//类型“Other”不可分配给类型“Parent”。属性“age”受到保护,但类型“Other”不是从“Parent”派生的类。6.泛型兼容性泛型包含类型参数,可以是任何类型,使用时类型参数会被指定为特定的类型,而这个类型只影响使用类型参数的部分:interfaceData{}letdata1:Data;letdata2:Data;data1=data2;//?data1和data2都是Data接口的实现,只是指定泛型参数的类型不同。TS是一个结构类型系统,所以上面data2对data1的赋值是兼容的,因为data2指定类型参数为字符串类型,但是在给参数T的接口中是没有用的,所以不管是不是字符串type传入或者number类型传入,再看一个例子:interfaceData{data:T;}letdata1:Data;letdata2:Data;data1=data2;//无法将类型“Data”分配给类型“Data”。“string”类型不能赋值给“number”类型现在结果不一样,赋值报错,因为data1和data2传入的泛型参数类型不同,导致结构不兼容。