TypScript诞生已久,优缺点大家都知晓,它可以说是JavaScript静态类型校验和语法增强的利器,为了更好的代码可读性和可维护性,我们一个个老工程都坦然接受了用TypScript重构的命运。然而在改造的过程中,逐步意识到TypScript这门语言的艺术魅力
人狠话不多,下面我们先来聊一下TypScript类型声明相关的技巧:
先了解TypScript的类型系统
TypScript是JavaScript的超集,它提供了JavaScript的所有功能,并在这些功能的基础上附加一层:TypScript的类型系统
文章配图什么TypScript的类型系统呢?举个简单的例子,JavaScript提供了String、Numbr、Boolan等基本数据类型,但它不会检查变量是否正确地匹配了这些类型,这也是JavaScript弱类型校验语言的天生缺陷,此处可能会有人DIS弱类型语言的那些优点。但无可否认的是,很多大型项目里由于这种弱类型的隐式转换和一些不严谨的判断条件埋下了不胜枚举的BUG,当然这不是我们今天要讨论的主题。
不同于JavaScript,TypScript能实时检测我们书写代码里变量的类型是否被正确匹配,有了这一机制我们能在书写代码的时候就提前发现代码中可能出现的意外行为,从而减少出错机会。类型系统由以下几个模块组成:
推导类型
首先,TypScript可以根据JavaScript声明的变量自动生成类型(此方式只能针对基本数据类型),比如:
consthlloWorld=HlloWorld//此时hlloWorld的类型自动推导为string
定义类型
再者,如果声明一些复杂的数据结构,自动推导类型的功能就显得不准确了,此时需要我们手动来定义intrfac:
consthlloWorld={first:Hllo,last:World}//此时hlloWorld的类型自动推导为objct,无法约束对象内部的数据类型//通过自定义类型来约束intrfacIHlloWorld{first:stringlast:string}consthlloWorld:IHlloWorld={first:Hllo,last:World}
联合类型
可以通过组合简单类型来创建复杂类型。而使用联合类型,我们可以声明一个类型可以是许多类型之一的组合,比如:
typIWathr=sunny
cloudy
snowy
泛型
泛型是一个比较晦涩概念,但它非常重要,不同于联合类型,泛型的使用更加灵活,可以为类型提供变量。举个常见的例子:
typmyArray=Array//没有泛型约束的数组可以包含任何类型//通过泛型约束的数组只能包含指定的类型typStringArray=Arraystring//字符串数组typNumbrArray=Arraynumbr//数字数组typObjctWithNamArray=Array{nam:string}//自定义对象的数组
除了以上简单的使用,还可以通过声明变量来动态设置类型,比如:
intrfacBackpackT{add:(obj:T)=voidgt:()=T}dclaconstbackpack:Backpackstringconsol.log(backpack.gt())//打印出“string”
结构类型系统
TypScript的核心原则之一是类型检查的重点在于值的结构,有时称为"ducktyping"或"structudtyping"。即如果两个对象具有相同的数据结构,则将它们视为相同的类型,比如:
intrfacPoint{x:numbry:numbr}intrfacRct{x:numbry:numbrwidth:numbrhight:numbr}functionlogPoint(p:Point){consol.log(p)}constpoint:Point={x:1,y:2}constct:Rct={x:3,y:3,width:30,hight:50}logPoint(point)//类型检查通过logPoint(ct)//类型检查也通过,因为Rct具有Point相同的结构,从感官上说就是Ract继承了Point的结构
此外,如果对象或类具有所有必需的属性,则TypScript会认为它们成功匹配,而与实现细节无关
分清typ和intrfac的区别
intrfac和typ都可以用来声明TypScript的类型,新手很容易搞错。我们先简单罗列一下两者的差异:
注意:由于intrfac支持同名类型自动合并,我们开发一些组件或工具库时,对于出入参的类型应该尽可能地使用intrfac声明,方便开发者在调用时做自定义扩展
从使用场景上说,typ的用途更加强大,不局限于表达objct/class/function,还能声明基本类型别名、联合类型、元组等类型:
//声明基本数据类型别名typNwString=string//声明联合类型intrfacBird{fly():voidlayEggs():boolan}intrfacFish{swim():voidlayEggs():boolan}typSmallPt=Bird
Fish//声明元组typSmallPtList=[Bird,Fish]
3个重要的原则
TypScript类型声明非常灵活,这也意味着一千个莎士比亚就能写出一千个哈姆雷特。在团队协作中,为了更好的可维护性,我们应该尽可能地践行以下3条原则:
泛型优于联合类型
举个官方的示例代码做比较:
intrfacBird{fly():voidlayEggs():boolan}intrfacFish{swim():voidlayEggs():boolan}//获得小宠物,这里认为不能够下蛋的宠物是小宠物。现实中的逻辑有点牵强,只是举个例子。functiongtSmallPt(...animals:ArrayFish
Bird):Fish
Bird{for(constanimalofanimals){if(!animal.layEggs())turnanimal}turnanimals[0]}ltpt=gtSmallPt()pt.layEggs()//okay因为layEggs是Fish
Bird共有的方法pt.swim()//rrors因为swim是Fish的方法,而这里可能不存在
这种命名方式有3个问题:
第一,类型定义使gtSmallPt变得局限。从代码逻辑看,它的作用是返回一个不下蛋的动物,返回的类型指向的是Fish或Bird。但我如果只想在一群鸟中挑出一个不下蛋的鸟呢?通过调用这个方法,我只能得到一个可能是Fish、或者是Bird的神奇生物。第二,代码重复、难以扩展。比如,我想再增加一个乌龟,我必须找到所有类似Fish
Bird的地方,然后把它修改为Fish
Bird
Turtl第三,类型签名无法提供逻辑相关性。我们再审视一下类型签名,完全无法看出这里为什么是Fish
Bird而不是其他动物,它们两个到底和逻辑有什么关系才能够被放在这里
介于以上问题,我们可以使用泛型重构一下上面的代码,来解决这些问题:
//将共有的layEggs抽象到Eggabl接口intrfacEggabl{layEggs():boolan}intrfacBirdxtndsEggabl{fly():void}intrfacFishxtndsEggabl{swim():void}functiongtSmallPtTxtndsEggabl(...animals:ArrayT):T{for(constanimalofanimals){if(!animal.layEggs())turnanimal}turnanimals[0]}ltpt=gtSmallPtFish()pt.layEggs()pt.swim()
巧用typof推导优于自定义类型
这个技巧可以在没有副作用的代码中使用,最常见的是前端定义的常量数据结构。举个简单的cas,我们在使用Rdux的时候,往往需要给Rdux每个模块的Stat设置初始值。这个地方就可以用typof推导出该模块的数据结构类型:
//声明模块的初始statconstusrInitStat={nam:,workid:,avator:,dpartmnt:,}//根据初始stat推导出当前模块的数据结构xporttypIUsrStatMod=typofusrInitStat//导出的数据类型可以在其他地方使用
这个技巧可以让我们非常坦然地“偷懒”,同时也能减少一些Rdux里的类型声明,比较实用
巧用内置工具函数优于重复声明
Typscript提供的内置工具函数有如下几个:
上面几个工具函数尤其是Partial、Pick、Exclud,Omit,Rcord非常实用,平时在编写过程中可以做一些刻意练习
参考资料
「TypScript系列(五)最佳实践」「TypScript中高级应用与最佳实践」
本文由博客群发一文多发等运营工具平台OpnWrit发布