前言
我们知道在Vue里用options来声明一个组件,举一个简单的例子
const options = {
props: {
name: {
type: String,
},
},
data() {
return {
score: 100
}
},
methods: {
click() {
this.score++
},
say() {
alert(`${this.name}: ${this.score}`)
}
},
}
export default options
这样在JavaScript里固然是没什么问题,但是在TS里就很郁闷了,因为options是一个plain object,涉及到函数的时候this的类型就很成问题。在上面的例子里,data()的this是一个字面量
{
props: {
name: {
type: StringConstructor
}
}
data(): {
score: number
}
methods: {
click(): void
say(): void
}
}
而click()的类型也是一个字面量
{
click(): void
say(): void
}
这样的类型虽然“很对”,但拿来是完全没有任何卵用的,而且click和say两个方法根本编译不过,因为他们的this上没有预期的name和score这些字段。
而预期的结果中,我们应该
1、在methods里能访问props里声明的字段、data()所返回的字段和methods里所声明的方法,这里的this就是
{
name: string
score: number
click(): void
say(): void
}
2、在data()里能访问props里声明的字段,这里的this就是
{
name: string
}
这篇文章就以上面的这组options为例子,来解释一下如何通过Contextual Typing和ThisType<T>工具类型获得正确的options类型声明。
正戏
这里的阶段性目标是要让options当中的这些函数声明具有“校正”过的this类型,需要用到TS内置的一个工具类型叫做ThisType<T>,官方的例子应该很好看懂。对着官方例子,这里先给methods对象打上预期的ThisType,那么Options就是
interface Options<Data, Props, Methods> {
props?: Props,
data?: Data
methods?: Methods & ThisType<Data & Props & Methods>
}
然后通过一个create工具函数来“诱导”它的类型
function create<Data, Props, Methods>(options: Options<Data, Props, Methods>) {
return options
}
注意这里的create函数什么也不干,直接return options,因为它没有任何实际值运算,而是为了利用TS的另一个特性叫做Contextual Typing来自动推导出Data, Props, Methods三个泛型参数的实际类型。
这时候代码可以写成
const options = create({
props: {
name: 'jim',
},
data: {
score: 100,
},
methods: {
click() {
this.score++
},
say() {
return `${this.name}: ${this.score}`
},
},
})
这个代码已经可以经过类型检验了,而剩下的过程则是一步步的把data和props变成预期的样子。
首先重新声明props的类型,成那个预期的嵌套描述的样子
interface Options<Data, Props, Methods> {
props?: {
[propName in keyof Props]: {
type: () => Props[propName]
}
}
data?: Data
methods?: Methods & ThisType<Data & Props & Methods>
}
这里的Props预期是一个plain object类型,在上面的例子里它的实际类型应该是
{
name: string
}
这时候类型已经对了,为了简化声明,可以抽取泛型
type PropType<T> = { (): T }
interface PropOption<T> {
type: PropType<T>
}
type PropsDefinition<T> = {
[propName in keyof T]: PropOption<T[propName]>
}
interface Options<Data, Props, Methods> {
props?: PropsDefinition<Props>
data?: Data
methods?: Methods & ThisType<Data & Props & Methods>
}
接下来是data的类型,先不考虑this,这里其实很简单,有两种办法。
第一种比较少动脑筋,可以说是头痛医头脚痛医脚,修改一下Options的声明
interface Options<DataInitializer extends () => {}, Props, Methods> {
data?: DataInitializer
props?: PropsDefinition<Props>
methods?: Methods & ThisType<ReturnType<DataInitializer> & Props & Methods>
}
data的类型是一个DataInitializer,再利用ReturnType<T>工具类型来用它的返回值类型表示Data类型。对应地,create的类型声明也要跟着改下
function create<DataInitializer extends () => {}, Props, Methods>(options: Options<DataInitializer, Props, Methods>) {
return
}
这样不怎么需要动脑筋,但是异常繁琐,既然已经有了上面推演props类型的经验,岂不是应该可以把它写成这样
interface Options<Data, Props, Methods> {
props?: PropsDefinition<Props>
data?: () => Data
methods?: Methods & ThisType<Data & Props & Methods>
}
function create<Data, Props, Methods>(options: Options<Data, Props, Methods>) {
return
}
很好,然后再限定它只能访问props里那些字段,手工显式指定就行
interface Options<Data, Props, Methods> {
props?: PropsDefinition<Props>
data?: (this: Props) => Data
methods?: Methods & ThisType<Data & Props & Methods>
}
稍微提取泛型重构一下
type DataDefinition<Data, Props> = (this: Props) => Data
interface Options<Data, Props, Methods> {
props?: PropsDefinition<Props>
data?: DataDefinition<Data, Props>
methods?: Methods & ThisType<Data & Props & Methods>
}
最终就可以愚快地使用create工具函数来对options进行带泛型推导的Contexual Typing了
const options = create({
props: {
name: {
type: String,
},
},
data() {
// this
return {
score: 100,
}
},
methods: {
click() {
this.score++
},
say() {
// this
return `${this.name}: ${this.score}`
},
},
})
export default options
可以放进playground或者vs code里尝试下,利用鼠标悬浮智能提示,可以看到不论是options还是data和methods里的this类型都能正确的推导。
以上代码就是Vue.extend的类型声明的一个极简版,阐述了它的基本思路,完整的Vue.extend代码大家有兴趣可以去vue源代码里找,主要在types/options.d.ts里面。
那么这个有啥用呢,最直观的一点是,如果你的代码里需要创建options,但又不方便用Vue.extend,而且是想创建一个“干净”的options——比方说,一个mixin——那么可以自己把Vue.extend的类型声明copy一份出来,然后像上面一样写一个“类型诱导”函数,直接return options。事实上@vue/composition-api就有类似的做法
这里的createComponent就是一个只为了获得Contexual Typing而写的透明的工具函数,因为截图里隐去了上面的大量类型声明,所以单独看这个return的话难免怀疑这是在搞笑。
讨好TypeScript编译器,可不比讨好女生容易啊……