phenomLi / Blog

Comments, Thoughts, Conclusions, Ideas, and the progress.
219 stars 17 forks source link

Typescript踩坑两则 #38

Open phenomLi opened 4 years ago

phenomLi commented 4 years ago

被ts的两个坑折磨多日,目前位置算是暂时解决了,遂记录之。

第一个是关于Typescript声明文件的。

用ts开发项目时,有时候会引用某些第三方库,比如JQuery等。然而第三方库都是js编写的(即使第三方库在开发时是用ts的最后也要编译为js),也就是说,编译器无法知道这些第三方库里面的函数或者变量的类型,同样得也失去了代码提示。所以声明文件就是为了解决这个问题而存在的。

简单来说声明文件就是用来声明js文件中的函数或变量的类型,通常以.d.ts后缀结尾。IDE通过访问声明文件,就能知道对应变量的类型。这里就不详细介绍声明文件了,具体可以看Typescript的官方文档


按官方文档所说,若项目用的是js,则声明文件需要手动编写,而如果项目是用ts写的,则可以在tsconfig中设置declaration: true自动生成声明文件。

正好我的项目是ts写的,按照上述做法,设置好tsconfig。编译,输出文件如下:


Emmm。。好像少了点东西。下图是我项目的目录,红框中的两个文件option.tssources.ts并没有对应的声明文件:


冷静分析.jpg


option.tssources.ts中仅有interface,没有任何类,函数或者对象。interface是ts独有的内容,在编译为js时,interface相关代码会被删除。因此,可以猜到问题有可能出在webpack那,因为项目中所有ts的编译打包都是webpack完成的,有可能webpack在生成声明文件时,把interface内容跳过了。

为了验证这一点,我这一次不使用webpack打包,直接用typescript编译器(tsc)编译。果然,文件一个不少:


但是之后,既要生成声明文件,又要用webpack编译打包,难道每次都要先手动敲tsc然后敲webpack这么麻烦吗?不怕,我们有npm script

package.json中,在script项添加配置如下:


其中tsc负责声明文件的生成,webpack负责ts文件的编译打包,&&表示两个命令先后执行。之后只要输入npm build即可先后执行两个命令。

当然,还没完,tsc读取的是项目下tsconfig的配置,其中包含了声明文件相关的配置项,因此webpack便不能再读取同一个tsconfig了,因为会造成冲突。webpack的ts-loader插件我用的是awesome-typescript-loader,查阅npm网站该插件的介绍,发现可以自定义tsconfig文件,牛逼。

于是乎,新建一个altconfig,只配置编译相关项:


然后在webpack中配置awesome-typescript-loader的options:


在tsconfig只保留声明文件相关配置:


问题解决。

第二个坑困扰了我最久,大概关于实现动态接口。


我项目中有一个比较“奇怪”的需求(可能也与我的架构设计有点关系)。其实说该需求奇怪,指的是在js环境下该写法十分自然,然而在ts环境下,难以用强类型进行约束。下面用简化的代码举一个例子:


如图,A中构造函数接受一个AI类型的data,然后把data的属性都复制到A中,但是在A的实例中访问AI的属性会报错。

至于为什么不直接A implements AI,是因为A在项目中是作为一个基类暴露给用户存在的,通常不直接使用,用户需要继承A进行扩展,编写独立的业务逻辑:


如上图的B,C。而B,C也有对应的BI,CI(继承于AI,内容也是由用户自定义)。如果每一个A的之类都要implements一次该类对应的interface,首先很麻烦,其次对于用户操作不够透明。该设计的目的是用户只需要在对应interface上声明data中有有什么内容,继承A后,data的内容会自动复制到类中称为类的成员。

如果每次都要用户手动implements,用户既要在interface写一遍属性,然后又要在类中再写一遍,增加了出错的机率,如属性字段错误,类型错误,属性缺失等(原则:永远不要相信用户),同时我本人觉得用户手动implements干涉了该设计的封装性,在我看来,用户不应该干涉黑盒工作。

上述这些在js中都不构成问题,开干就完事了。然而对于ts编译器,并不知道A中含有AI的属性,因为构造函数中的属性复制对于ts编译器是”隐式“操作,故在A的实例中访问AI的属性发生了报错。


那么如何解决呢?开始的想法我认为想到编译器知道A中有AI的属性,必须要显式的进行implements,但是不是用户手动,而是使用泛型动态implements:


然而该方法无论怎么改都会报错,显然ts是不支持这种写法的。之后我在segmentfault提了这个问题,唯一的一个回答中给到了另一种思考方式:


该方便把属性复制操作单独拿出来成为一个静态方法,interface类型作为泛型T,然后用交叉类型A & T表示属性复制后的A。其实这是一个完全可行的方法,保证了类型安全的同时也避免了implements。然而我最后并没有采用,因为该方法需要使用一个额外的类型来封装A & T(暂且假设为AT)。我这个项目定位是一个框架,是需要用户花成本学习的,原本用户需要理解的类型只有A和AI,那现在就变成了A,AI,AT。增加一个类型后面会牵涉到众多繁杂的概念,用户学习成本陡增。

你可能会问:为什么需要AT?让用户直接写A & T不行吗?ok,那用户在编写他们的代码时,遇到A & T,框架需要给用户普及什么是交叉类型吗,这只是typescript文档需要做的事。即使用户拥有扎实的ts基础,但是他需要思考这个类型为什么是A,T交叉吗。当用户思考到这一步时,已经意味着越界了,同时这也是框架/库设计者不想看见的情况: 框架/库把某些“drity”部分暴露给了用户,用户被其所困扰。 用户其实不需要思考这些,他们只需要作为框架/库的上层,使用简明的接口,专注于解决业务。


那么最后我是怎么解决这个问题的呢?最后我并没有解决,我只是用了一种障眼法骗了骗编译器,让其不报错,其余代码提示类型安全什么的,就先不管了:


算是一种妥协的办法把,毕竟this[prop] = data[prop]是很动态的写法了,与ts的初衷有些背道而驰。

另外,我在知乎的提问有一个答案和我的妥协办法有些相似:


相比我的方法,他增加了值约束T[K],不过在我看来有些多余了,写了[key: string]注定失去了属性约束,在没有属性约束的情况下值约束意义不大。


--- EOF ---

Ctrl-Ling commented 4 years ago

又踩坑啦🌚

ystarlongzi commented 3 years ago

.....还没完,tsc读取的是项目下tsconfig的配置,其中包含了声明文件相关的配置项,因此webpack便不能再读取同一个tsconfig了,因为会造成冲突....

这里的「冲突」,会带来什么副作用?重复生成两次类型文件?