在Vue中给通过this.$refs引用的自定义控件添加类型声明

2022-02-28 18:19:32 浏览数 (2)

0x00 hello world

最近在一个新项目中,尝试了vue2 typescript的组合,又又又碰到一个问题:定义了一个自定义控件Foo.vue,在控件中定义一个方法Bar(),使用自定义控件的时候,添加ref='foo'并且希望通过使用this.$refs.foo.Bar()调用方法,当然是可以成功调用的,但是在TypeScript中,他会报错。

图一图一

后来我折腾了好久,想出了一个不是那么优雅的方法:

图2图2

这个样子,虽然不报错了,但是生生的把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();,居然可以!就觉得很神奇!这什么意思呀。

图3图3

0x02 原理?

为了搞明白这到底是什么意思,我研究了一下vue的类型定义文件

Vue.extend的定义如下:

代码语言:typescript复制
  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,他的定义如下:

代码语言:typescript复制
export type ExtendedVue<Instance extends Vue, Data, Methods, Computed, Props> = VueConstructor<CombinedVueInstance<Instance, Data, Methods, Computed, Props> & Vue>;

返回的是一个VueConstructor,他的定义如下:

代码语言:typescript复制
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的函数:

代码语言:typescript复制
  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,这个类型的定义如下:

代码语言:typescript复制
export type CombinedVueInstance<Instance extends Vue, Data, Methods, Computed, Props> =  Data & Methods & Computed & Props & Instance;

这个 CombinedVueInstance 的用处就是把组件定义的内容和Vue本身的内容组合到一起。

0x03 总结

总结下来就是:

  1. 在JavaScript中,一个东西(函数?类型?)的类型有两种,一种是他本来的类型,一种是实例化之后的实例类型,这两个类型有可能是不一样的;
  2. Vue的类型和Vue实例化的后的类型不是同一个类型,Vue的类型是VueConstructor类型,实例化后的类型是CombinedVueInstance
  3. 我需要的是一个实例化之后的类型,所以Foo是我导入的一个变量,通过type of Foo取得它的类型,但是,但是我需要的是它实例化后的类型,所以还需要通过InstanceType<typeof Foo>获取。

0x04 特别感谢

感谢TDP成员若海 在这个过程中给我的无私帮助!

腾云先锋(TDP,Tencent Cloud Developer Pioneer)是腾讯云GTS官方组建并运营的技术开发者群体。这里有最专业的开发者&客户,能与产品人员亲密接触,专有的问题&需求反馈渠道,有一群志同道合的兄弟姐妹。

有兴趣的朋友可以关注 腾云先锋团队 加入TDP。

0 人点赞