在Vue中给通过this.$refs引用的自定义控件添加类型声明
0x00 hello world
最近在一个新项目中,尝试了vue2+typescript的组合,又又又碰到一个问题:定义了一个自定义控件Foo.vue
,在控件中定义一个方法Bar()
,使用自定义控件的时候,添加ref='foo'
并且希望通过使用this.$refs.foo.Bar()
调用方法,当然是可以成功调用的,但是在TypeScript中,他会报错。
后来我折腾了好久,想出了一个不是那么优雅的方法:
这个样子,虽然不报错了,但是生生的把TypeScript写成了AnyScript,如果我修改了Bar的定义,比如添加了一个参数,这边就不会提示错误,告诉我缺一个参数,就失去了使用TypeScript的意义。
0x01 优雅的方式
我试过(this.$refs.foo as Foo).Bar();
不行,试过(this.$refs.foo as typeof Foo).Bar();
不行,鬼使神差的,我试了下(this.$refs.foo as InstanceType<typeof Foo>).Bar();
,居然可以!就觉得很神奇!这什么意思呀。
0x02 原理?
为了搞明白这到底是什么意思,我研究了一下vue的类型定义文件
Vue.extend的定义如下:
extend<Data, Methods, Computed, PropNames extends string = never>(options?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): ExtendedVue<V, Data, Methods, Computed, Record<PropNames, any>>;
extend<Data, Methods, Computed, Props>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>;
extend<PropNames extends string = never>(definition: FunctionalComponentOptions<Record<PropNames, any>, PropNames[]>): ExtendedVue<V, {}, {}, {}, Record<PropNames, any>>;
extend<Props>(definition: FunctionalComponentOptions<Props, RecordPropsDefinition<Props>>): ExtendedVue<V, {}, {}, {}, Props>;
extend(options?: ComponentOptions<V>): ExtendedVue<V, {}, {}, {}, {}>;
返回的是一个ExtendedVue,他的定义如下:
export type ExtendedVue<Instance extends Vue, Data, Methods, Computed, Props> = VueConstructor<CombinedVueInstance<Instance, Data, Methods, Computed, Props> & Vue>;
返回的是一个VueConstructor,他的定义如下:
export interface VueConstructor<V extends Vue = Vue> {
new <Data = object, Methods = object, Computed = object, PropNames extends string = never>(options?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): CombinedVueInstance<V, Data, Methods, Computed, Record<PropNames, any>>;
// ideally, the return type should just contain Props, not Record<keyof Props, any>. But TS requires to have Base constructors with the same return type.
new <Data = object, Methods = object, Computed = object, Props = object>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): CombinedVueInstance<V, Data, Methods, Computed, Record<keyof Props, any>>;
new (options?: ComponentOptions<V>): CombinedVueInstance<V, object, object, object, Record<keyof object, any>>;
extend<Data, Methods, Computed, PropNames extends string = never>(options?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): ExtendedVue<V, Data, Methods, Computed, Record<PropNames, any>>;
extend<Data, Methods, Computed, Props>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>;
extend<PropNames extends string = never>(definition: FunctionalComponentOptions<Record<PropNames, any>, PropNames[]>): ExtendedVue<V, {}, {}, {}, Record<PropNames, any>>;
extend<Props>(definition: FunctionalComponentOptions<Props, RecordPropsDefinition<Props>>): ExtendedVue<V, {}, {}, {}, Props>;
extend(options?: ComponentOptions<V>): ExtendedVue<V, {}, {}, {}, {}>;
nextTick<T>(callback: (this: T) => void, context?: T): void;
nextTick(): Promise<void>
set<T>(object: object, key: string | number, value: T): T;
set<T>(array: T[], key: number, value: T): T;
delete(object: object, key: string | number): void;
delete<T>(array: T[], key: number): void;
directive(
id: string,
definition?: DirectiveOptions | DirectiveFunction
): DirectiveOptions;
filter(id: string, definition?: Function): Function;
component(id: string): VueConstructor;
component<VC extends VueConstructor>(id: string, constructor: VC): VC;
component<Data, Methods, Computed, Props>(id: string, definition: AsyncComponent<Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>;
component<Data, Methods, Computed, PropNames extends string = never>(id: string, definition?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): ExtendedVue<V, Data, Methods, Computed, Record<PropNames, any>>;
component<Data, Methods, Computed, Props>(id: string, definition?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>;
component<PropNames extends string>(id: string, definition: FunctionalComponentOptions<Record<PropNames, any>, PropNames[]>): ExtendedVue<V, {}, {}, {}, Record<PropNames, any>>;
component<Props>(id: string, definition: FunctionalComponentOptions<Props, RecordPropsDefinition<Props>>): ExtendedVue<V, {}, {}, {}, Props>;
component(id: string, definition?: ComponentOptions<V>): ExtendedVue<V, {}, {}, {}, {}>;
use<T>(plugin: PluginObject<T> | PluginFunction<T>, options?: T): VueConstructor<V>;
use(plugin: PluginObject<any> | PluginFunction<any>, ...options: any[]): VueConstructor<V>;
mixin(mixin: VueConstructor | ComponentOptions<Vue>): VueConstructor<V>;
compile(template: string): {
render(createElement: typeof Vue.prototype.$createElement): VNode;
staticRenderFns: (() => VNode)[];
};
observable<T>(obj: T): T;
util: {
warn(msg: string, vm?: InstanceType<VueConstructor>): void;
};
config: VueConfiguration;
version: string;
}
注意前面3行,那几个名字为new的函数:
new <Data = object, Methods = object, Computed = object, PropNames extends string = never>(options?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): CombinedVueInstance<V, Data, Methods, Computed, Record<PropNames, any>>;
// ideally, the return type should just contain Props, not Record<keyof Props, any>. But TS requires to have Base constructors with the same return type.
new <Data = object, Methods = object, Computed = object, Props = object>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): CombinedVueInstance<V, Data, Methods, Computed, Record<keyof Props, any>>;
new (options?: ComponentOptions<V>): CombinedVueInstance<V, object, object, object, Record<keyof object, any>>;
也就是说new一个Vue实例的时候,返回的类型是CombinedVueInstance
,这个类型的定义如下:
export type CombinedVueInstance<Instance extends Vue, Data, Methods, Computed, Props> = Data & Methods & Computed & Props & Instance;
这个 CombinedVueInstance
的用处就是把组件定义的内容和Vue本身的内容组合到一起。
0x03 总结
总结下来就是:
- 在JavaScript中,一个东西(函数?类型?)的类型有两种,一种是他本来的类型,一种是实例化之后的实例类型,这两个类型有可能是不一样的;
- Vue的类型和Vue实例化的后的类型不是同一个类型,Vue的类型是
VueConstructor
类型,实例化后的类型是CombinedVueInstance
; - 我需要的是一个实例化之后的类型,所以
Foo
是我导入的一个变量,通过type of Foo
取得它的类型,但是,但是我需要的是它实例化后的类型,所以还需要通过InstanceType<typeof Foo>
获取。
0x04 特别感谢
感谢TDP成员若海 在这个过程中给我的无私帮助!
腾云先锋(TDP,Tencent Cloud Developer Pioneer)是腾讯云GTS官方组建并运营的技术开发者群体。这里有最专业的开发者&客户,能与产品人员亲密接触,专有的问题&需求反馈渠道,有一群志同道合的兄弟姐妹。
有兴趣的朋友可以关注 腾云先锋团队 加入TDP。
![](https://jkboy.com/wp-content/uploads/2021/10/Pasted-11.png)