自从去年开始在项目里写了一段时间 Javascript 后,感觉没有类型检查的语言还是不太适合我,所以一直想尝试下 TypeScript,然而由于项目庞大,人员协作问题,一时半会没办法切成 TypeScript。正好最近有小程序的需求和小程序去年 11 月开始官方支持了 TypeScript,所以拿来练练手。
Why TypeScript?
大概是我这半年写的 Swift 比较多,而 Swift 中的静态类型和协议是我很喜欢的特性。正好 TypeScript 为 JavaScript 带来了静态类型和接口。
可选的静态类型
“动态类型一时爽,代码重构火葬场“,对于我这种极度喜欢重(xia)构(gai)代码的人来说,JavaScript 毫无类型提示,类型全靠命名猜测是极度不友好的。而 TypeScript 加上了灵活的类型系统,不仅可以编码期检查,还能增强代码的可读性,并提供了 any 类型进行缓冲。
接口
接口和协议,只是不一样的叫法而已,Java、C#、TypeScript 叫 Interface,Swift、Kotlin 叫 Protocol,就是一种规则声明。项目中,和后端接口数据交互,页面传递数据,数据持有,方法代理的地方,有了接口就会更加方便,易重构。TypeScript 的 Interface + JavaScript 简单的对象就让数据构建变得简单又不容易出错。
小程序对 TypeScript 的支持
TypeScript 有一个很重要的东西,就是 d.ts 文件。d.ts 文件其实相当于 C 系语言里面的 .h 头文件,声明了对外暴露的方法和属性。而小程序官方对 TypeScript 的支持,意味着官方会维护小程序本身 API 的 d.ts 文件,也就是 typing 库,这样当 API 发生变动时,就可以即时变更。
使用也很简单,更新微信开发者工具到最新版,在创建新项目时选择 TypeScript 模板。
创建后,我们可以看到项目里带上了 typings 库,以及 TypeScript 的配置文件 tsconifg。之后,保存时就不会自动编译了,要点击小程序工具栏的编译按钮才可以。
这里有一个坑,笔者电脑安装的 TypeScript 版本是 3.2.2 版本。编译时会被找不到全局类型 CallableFunction 和 NewableFunction。
解决方法也很简单,到 node_modules 路径下的 TypeScript 包的 bin 目录下,lib.es5.d.ts 文件里面把这两个类型的 Interface 拷贝到,小程序 typing 目录下的 lib.wa.es6.d.ts 里面就可以了。小程序模板里这个文件应该是拷贝 TypeScript 官方的,但没有随着官方升级而改变。
事件
视图的事件,对应的类型笔者在 typings 中并没有看到有 Interface 定义,所以只能暂时用 any,然后自己再用
as 转一下 event 携带的数据的类型。
Page&Data
每个 Page 对象,在 typing 里是这么定义的。
declare const Page: Page.PageConstructor
interface PageConstructor {
<D extends IAnyObject, T extends IAnyObject & PageInstance>(
options: PageInstance<D, T> & T
): void
}
也就是说,它支持 D 和 T 两个范型。这两个范型是什么呢?小程序里,Page 是这么写的。
Page({});
也就是说,options 参数就是一个 PageInstance,范型也被传入了。
interface PageInstance<D extends IAnyObject = any, T extends IAnyObject = any> extends PageInstanceBaseProps<D>
PageInstance 里面定义了 Page 声明周期的方法,而且继承自 PageInstanceBaseProps,并将范型 D 传入。
interface PageInstanceBaseProps<D extends IAnyObject = any> {
data?: D
//...
}
所以这个 D 范型,其实就是 data 的类型接口。因为 data 不是必须实现的,所以这里是可选型 ?。
那么 T 是什么呢?
T extends IAnyObject & PageInstance
T 其实就是对 PageInstance 的拓展,PageInstance 是 Page 的实例接口,那么 T 其实就是在 Page 里面 this 的类型接口了,也就是说,需要在 Page 里新增的方法和属性,都在 T 里定义。
所以,对于一个普通页面我们可以声明两个接口,一个代表 data, 一个代表 page,举个例子。
interface IIntroPage {
nextButtonTap(event: any): void;
isLoading: boolean;
}
interface IIntroData {
test: string;
}
Page<IIntroData, IIntroPage>({
isLoading: false,
nextButtonTap(event: any) {
this.isLoading = true;
}
});
如果这个页面不需要 data 或者不需要扩展 page,用 IAnyObject 代替 D 或者 T 即可。
interface PageInstanceBaseProps<D extends IAnyObject = any> {
data?: D
setData?<K extends keyof D>(
data: D | Pick<D, K> | IAnyObject,
callback?: () => void
): void
}
同时,由于 setData 和 data 都被声明为可选项,使用时需要加上!,this.setData!({})
和 this.data!
。
其他就没什么了,用上 TypeScript 之后,官方的 API 都可以直接看参数和返回值的类型,再也不用去查文档猜测类型了。
调用 JavaScript
为 JavaScript 编写一个简单的 .d.ts 文件,将需要调用的类和方法暴露出来。详情见如何编写一个d.ts文件。
最后
虽然笔者用了 TypeScript 不久,但严格的检查的确让我在增删改接口字段能快速全局重构,而且方法调用联想,API 查看也方便了不少。小程序对 TypeScript 的支持日常使用开发是没有什么问题了,就是官方的文档指引比较少。