当前位置: 首页 > Web前端 > JavaScript

TypeScript:老手容易混淆的地方

时间:2023-03-27 12:00:36 JavaScript

首先需要说明一下,因为TypeScript的类型系统最终是服务于JavaScript的,所以任何js、ts写的代码都必须能够声明对应的类型约束,这就导致了ts可以出现非常复杂的类型声明。但是一般的强类型语言都不存在这样的问题,因为在设计之初,那些不能用类型系统声明的接口是根本不允许创建的。此外,TypeScript是一种结构类型,不同于标称类型。任何类型是否适合取决于其结构是否适合,而标称类型必须具有严格的类型对应关系。下面的很多内容都是基于以上两点来思考的,进入正题。1、关于枚举除了正常枚举的声明外,枚举的声明在面对不同的场景时也会有些不同。动态枚举允许在枚举中初始化动态值,但是字符串不能//动态值enumA{Error=Math.random(),Yes=3*9}//enumisnotpossiblewithstringsA{Error=Math.random(),//错误:计算的值不允许出现在包含字符串值成员的枚举中variablereferencesconstenumNoYes{No='No',Yes='Yes'}//编译前:consta=NoYes.NO//编译后:consta='No'asanobject//因为ts是一个结构体类型系统,枚举也可以作为对象传入,但总感觉怪怪的}函数(否);//编译通过不同的宽松检查传递数字和字符串枚举//数字宽松检查enumNoYes{No,Yes}functionfunc(noYes:NoYes){}func(33);//不会报类型错误!//字符串枚举报错enumNoYes{No='No',Yes='Yes'}functionfunc(noYes:NoYes){}func('NO');//错误:类型““NO””不能将参数分配给类型“NoYes”的参数。之所以允许随意给枚举赋值,我猜也是因为允许动态数值枚举。2、为什么重载不能单独写?ts中的函数重载:functionfoo(p:string);函数foo(p:数字);functionfoo(p:string|number){...};加载:公共类重载{publicintfoo(){System.out.println("test1");返回1;}publicvoidfoo(inta){System.out.println("test2");}}不支持单独写重载的原因是:传统的重载是在编译时将重载的函数拆分成名字(func分为func1和func2),然后在调用处修改名字,从而通过参数达到区分调用的效果。但是,JavaScript可以在运行时随时修改类型。如果仍然沿用传统的重载编译规则,可能会出现意想不到的问题。ts和js之间的交互性受到影响。如果函数像传统重载那样被拆分,那么在js脚本中调用ts中重载的方法就会出现问题。所以相比于传统的重载,ts重载更像是一种类型注解。3.为什么会有?想象一个场景,一个函数需要接受一个数组,数组中的数据可以是任意类型。如果要用泛型,也不能完美解决,所以要引入any,后面引入unknown。替换任何。但是在一般的强类型语言中,通常没有那么大的灵活性。比如数组只允许一种类型,可以用泛型来解决。JSON.parse的类型声明也是any,因为一开始就没有unknown,不然返回unknown比较合理。4.图灵完备的类型系统TypeScript为了不削弱JavaScript的灵活性,同时也提供足够的类型约束,它带来了一个图灵完备的类型系统。下面使用类型实现一个数字元组自动声明N的长度,过程中需要用到递归类型ToArr=Arr['length']extendsN//判断数组的长度是否达到?arr//如果长度足够,直接返回:ToArr;//如果长度不够,递归类型Arr1=ToArr<3>;//[number,number,number]更进一步,甚至在上面的ToArr基础上添加:typeAdd=[...ToArr,...ToArr]['长度'];输入Res=添加<3,4>;//7甚至有人用ts的类型系统实现了国际象棋规则。五、readonly和asconstreadonly和asconst可以把类型声明为只读,asconst也可以把类型转换成常量。让我们看看两者的一些细节。interfaceFoo{readonlya:{b:number,},}constf:Foo={a:{b:1},};f.a={b:2};//错误:无法分配给“a”,因为它是只读属性f.a.b=2;//这里没有问题从上面可以看出,readonly只对当前对象有效,对其属性无效。但是readonly可以让数组完全不可修改。constarr:readonlynumber[]=[2];arr.push(1);//错误:“readonlynumber[]”类型上不存在属性“push”,因为const将数组转换为元组,并将可变长度数组声明变为固定长度数组:constargs=[8,5];//number[]constfunc=(x:number,y:number)=>{};constangle=func(...args);//这里会提示错误,因为ts不确定args是否有两个数constargs=[8,5]asconst;//添加为const,将args转换为[number,number]args.push(2)//错误:属性“push”在类型“readonly[8,5]”上不存在6.类型约束重置缩小回调函数中的类型约束会被重置,因为回调可能在异步代码之后被调用,通过闭包访问的变量有被改变的风险,所以约束重置是合理的。有关详细信息,请参见以下示例:typeMyType={prop?:number|细绳,};functionfunc(arg:MyType){if(typeofarg.prop==='string'){consta=arg.prop;//string[].forEach(()=>{//如果这是一个异步回调,重写下面的变量会导致这里的arg改变,所以约束重置是合理的constb=arg.prop;//string|number|undefinedconsole.log(b);});(()=>{//立即执行函数而不重置类型约束constd=arg.prop;//stringconsole.log(d);})();//重写变量arg={};}}这也是为了应对js的灵活性所需要的严谨。7、Covarianceandcontravariance协变:子类型兼容父类型,即Array.push(Son),可以成立,因为Son是Father的子类型,继承了Father的所有属性,所以因为它是兼容的;inversion:父类型兼容子类型,与上面相反,详见下面的例子;声明letanimalFn:(x:Animal)=>void;函数walkdog(fn:(x:Dog)=>void){}walkdog(animalFn);//这里OK,animalFn的参数声明需要Dog,但是实际输入的是Animal,上面的本质是(x:Dog)=>void=(x:Animal)=>void,所以给参数Animal赋值为Dog,所以Animal与Dog兼容,即倒置。这一幕要是反过来,那就不对了。所以函数的参数是逆变的,返回值是协变的。但是ts的函数类型其实是双向协变的,但这并不安全,具体看下面的例子:declareletanimalFn:(x:Animal)=>voiddeclareletdogFn:(x:Dog)=>voidanimalFn=dogFn//OK,但这不安全dogFn=animalFn//OK//虽然像上面这样的双向赋值(双向协方差)在ts中是可以通过的,但它是不安全的constanimalSpeak=(fn:AnimalFn)=>{fn(动物);};animalSpeak((x:Dog)=>{x.Wow()//这里会报错,因为传入的Animal没有dog.Wow方法});上面调用animalSpeak实际上Animal是作为参数赋值给Dog的,不符合函数参数倒置的原则,但是在ts中是可以编译的。为什么ts允许函数的双向协变:因为ts是结构化语言,如果Array(Dog)可以赋给Array(Animal),那么就意味着Array(Dog).push可以赋给Array(Animal)。push,导致设计允许双向协变,这是ts设计者为了保持结构化类型的兼容性而做出的权衡。但是双向协变毕竟不安全,所以在2.6版本之后,如果开启严格模式,函数参数协变会报错。对于双向协方差,你可以看下面的例子:;animalArr=dogArr;//OK//Array.push(Animal):number=Array.push(Dog):number(参数协方差)animalArr.push=dogArr.push;//OK八、除了Type约束,TypeScript还带来了更完善的面向对象,因为前端的一部分复杂度被MVVM框架消化了,以往js的面向对象模拟有些问题,所以我经常忽略面向对象的价值,但是ts带来了更完善和安全的封装、继承和多态。所以以后写ts的时候提醒自己,可以多考虑??编码。参考:https://exploringjs.com/tackl...https://www.zhihu.com/questio...https://jkchao.github.io/type...https://zhuanlan.zhihu。com/p/...