找回密码
 会员注册
查看: 32|回复: 0

TypeScriptnever和unknown的优雅之道

[复制链接]

13

主题

0

回帖

40

积分

新手上路

积分
40
发表于 2024-9-20 18:39:13 | 显示全部楼层 |阅读模式
1、前言 TypeScript在版本2.0和3.0分别引入了“never”和“unknown”两个基本类型,在引入这两个类型之后,TypeScript的类型系统得到了极大的完善。但在我平时接手代码的时候,我发现很多同学的观念还停留在1.0的时代,那个any大法好的时代。毕竟JavaScript是一门弱类型动态语言,我们以往不会投入过多的时间去关注类型设计。在引入TypeScript之后,我们甚至还会抱怨:“这代码怎么还越写越多了?”。其实我们应该反过来思考,OOP的编程范式,才是ES6后的代码应该有的模样。2、TypeScript中的toptype、bottomtype在类型系统设计中,有两种特别的类型:Toptype:被称为通用父类型,也就是能够包含所有值的类型。Bottomtype:代表没有值的类型,它也被称为零或空类型,是所有类型的子类型。按照类型系统的解释,在TypeScript3.0中,有两个toptype(any和unknown)和一个bottomtype(never)。但也有一些人认为,any也是一个bottomtype,因为any也可以作为很多类型的子类型。但这种说法其实并不严格,我们可以深入了解一下unknown、any、never这三个类型。3、unknown和any3.1unknown——代表万物我在阅读同事的代码时,很少看到unknown类型的出现。这并不意味着它不重要,相反,它是安全版本的any类型。它和any的区别很简单,参考下面的例子:functionformat1(value:any){value.toFixed(2);//不飘红,想干什么干什么,verydangerous}functionformat2(value:unknown){value.toFixed(2);//代码会飘红,阻止你这么做//你需要收窄类型范围,例如://1、类型断言——不飘红,但执行时可能错误(valueasNumber).toFixed(2);//2、类型守卫——不飘红,且确保正常执行if(typeofvalue==='number'){//推断出类型:numbervalue.toFixed(2);}//3、类型断言函数,抛出错误——不飘红,且确保正常执行assertIsNumber(value);value.toFixed(2);}/**类型断言函数,抛出错误*/functionassertIsNumber(arg:unknown):assertsargisNumber{if(!(arginstanceofNumber)){thrownewTypeError('NotaNumber:'+arg);}}使用any好比鬼屋探险,代码执行的时候处处见鬼。而unknown结合类型守卫等方式,可以确保上游数据结构不确定时,也能让代码正常执行。3.2any——一丝不挂我们用到any,就意味着放弃类型检查了,因为它不是用来描述具体类型的。在使用它之前,我们需要想两件事:能否使用更具体的类型能否使用unknown代替都不能的情况下,any才是最后的选择。3.3回顾以前的类型设计现有的一些类型设计用到了any,其实不够准确。这里举两个例子:3.3.1String()String() 能够接受任何参数,转化为字符串。结合上文介绍的unknown类型,其实这里的参数也可以设计成unknown,但内部实现就需要多设计些类型守卫了。但unknown类型是后面才出现的,所以一开始的设计还是采用了any,也就是我们现在看到的:/***typescript/lib/lib.es5.d.ts*/interfaceStringConstructor{new(value?:any):String;(value?:any):string;readonlyprototype:String;fromCharCode(...codes:number[]):string;}3.3.2JSON.parse()最近我写了一段涉及深拷贝的代码:exportfunctiondeleteCommentFromComments(comments:GenericsComment[],comment:GenericsComment){//深拷贝constlist:GenericsComment[]=JSON.parse(JSON.stringify(comments));//找到对应的评论下标consttargetIndex=list.findIndex((item)=>{if(item.comment_id===comment.comment_id){returntrue;}returnfalse;});if(targetIndex!==-1){//剔除对应的评论list.splice(targetIndex,1);}returnlist;}很明显,JSON.parse()的输出是随着输入动态改变的(甚至有可能抛出Error),它的函数签名被设计成了:interfaceJSON{parse(text:string,reviver?this:any,key:string,value:any)=>any):any;...}这里可以用unknown嘛?可以,不过原因和上面一样,JSON.parse() 的函数签名被添加到TypeScript系统之前,unknown类型还没出现,否则它的返回类型应该是unknown。4、never上文提到,never 类型表示的是空类型,也就是值永不存在的类型。值会永不存在的两种情况:如果一个函数执行时抛出了异常,那么这个函数永远不存在返回值(因为抛出异常会直接中断程序运行,这使得程序运行不到返回值那一步,即具有不可达的终点,也就永不存在返回了);函数中执行无限循环的代码(死循环),使得程序永远无法运行到函数返回值那一步,永不存在返回。//异常functionerr(msg:string):never{//OKthrownewError(msg);}//死循环functionloopForever():never{//OKwhile(true){};}4.1唯一的bottomtype由于never是typescript的唯一一个bottomtype,它能够表示任何类型的子类型,所以能够赋值给任何类型:leterr:never;letnum:number=4;num=err;//OK我们可以使用集合来理解never,unknown是全集,never是最小单元(空集),任意类型都包含了never。4.1.1null/undefined和never这里可能就要问了,null和undefined好像也可以表示任何类型的子类型,为啥不是bottomtype。非也,never特殊就特殊在,除了自身以外,没有任何类型是它的子类型,或者说可以赋值给它。它才是人下人(狗头),我们可以用下面的例子对比看看://null和undefined,可以被never赋值declareconstn:never;leta:null=n;//正确letb:undefined=n;//正确//never是bottomtype,除了自己以外没有任何类型可以赋值给它letne:never;ne=null;//错误ne=undefined;//错误declareconstan:any;ne=an;//错误,any也不可以declareconstnev:never;ne=nev;//正确,只有never可以赋值给never上面的例子基本上说明了null/undefined跟never的区别,never才是最bottom的。4.1.2为什么说any不是严格的bottomtype我在阅读一些文章的时候发现,大家常说any既是toptype,也是bottomtype,但这种说法并不严谨。从上文我们知道,除了never自身,没有任何类型能赋值给never。any是否满足这个特性呢?显然不能,举个很简单的例子:consta='anything';constb:any=a;//能够赋值constc:never=a;//报错,不能赋值而我们为什么说never才是bottomtype?维基百科上这样解释:Afunctionwhosereturntypeisbottom(presumably)cannotreturnanyvalue,noteventhezerosizeunittype.Thereforeafunctionwhosereturntypeisthebottomtypecannotreturn.返回类型为底部类型的函数不能返回任何值,甚至不能返回零大小的单元类型。因此返回类型为底部类型的函数不能返回。从这里我们也很容易发现,在一个类型系统中,bottomtype是独一无二的,它唯一地描述了函数无返回的情况。所以,有了never之后,any这种脱离了类型检查的异端肯定称不上是bottomtype。4.2never的妙用never有以下的使用场景:Unreachablecode检查:标记不可达代码,获得编译提示。类型运算:作为类型运算中的最小因子。ExhaustiveCheck:为复合类型创造编译提示。......关于never的用途,知乎上有个很好的讨论。不可否认的是,never这个东西很奇妙,从集合论的角度,它是一个空集合,因此它可以通过空集合的一些特性,为我们的类型运算工作带来很大便利。接下来来具体讲讲各个使用场景:4.2.1Unreachablecode检查一个萌新写出了下面这行代码:process.exit(0);console.log("helloworld")//Unreachablecodedetected.ts(7027)不要笑,是真的有可能。当然这时候如果你使用了ts,它会给你一个编译器提示:Error:Unreachablecodedetected.ts(7027)因为 process.exit() 返回类型被定义为了never,在它之后的自然就是「unreachablecode」了。其他可能的场景还有,监听套接字:functionlisten():never{while(true){letconn=server.accept();}}listen();console.log("!!!");//Unreachablecodedetected.ts(7027)通常来说,我们手动标记函数返回值为never类型,来帮助编译器识别「unreachablecode」,并帮助我们收窄(narrow)类型。下面是一个没标记的例子:functionthrowError(){thrownewError();}functionfirstChar(msg:string|undefined){if(msg===undefined)throwError();letchr=msg.charAt(1)//Objectispossibly'undefined'.}由于编译器不知道throwError是一个无返回的函数,所以 throwError() 之后的代码被认为在任意情况下都是可达的,让编译器误会msg的类型是string|undefined。这时候如果标记上了never类型,那么msg的类型将会在空检查之后收窄为string:functionthrowError():never{thrownewError();}functionfirstChar(msg:string|undefined){if(msg===undefined)throwError();letchr=msg.charAt(1)//✅}4.2.2类型运算4.2.2.1最小因子上文提到never可以理解为一个空集,那么它将满足下面的运算规则:T|never=>TT&never=>never也就是说,never是类型运算的最小因子。这些规则帮助我们简化了一些琐碎的类型运算,举个例子,像 romise.race 合并的多个 romise,有时是无法确切知道时序和返回结果的。现在我们使用一个 romise.race 来将一个有网络请求返回值的 romise 和另一个在给定时间之内就会被 reject 的 romise 合并起来。asyncfunctionfetchNameWithTimeout(userId:string)romise{constdata=awaitPromise.race([fetchData(userId),timeout(3000)])returndata.userName;}下面是一个timeout函数的实现,如果超过指定时间,将会抛出一个Error。由于它是无返回的,所以返回结果定义为了 romise:functiontimeout(ms:number)romise{returnnewPromise((_,reject)=>{setTimeout(()=>reject(newError("Timeout!")),ms)})}很好,接下来编译器会去推断 romise.race 的返回值,因为race会取最先完成的那个 romise 的结果,所以在上面这个例子里,它的函数签名类似这样:functionrace
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-27 00:09 , Processed in 1.962012 second(s), 27 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表