Skip to content

Typescript踩坑两则 #38

Description

@phenomLi

被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 ---

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions