kaorun343 / vue-property-decorator

Vue.js and Property Decorator
MIT License
5.52k stars 380 forks source link

Method does't exist in $ref component #257

Open cawa-93 opened 5 years ago

cawa-93 commented 5 years ago

Example:

// Child component
@Component

export default class CommentForm extends Vue {
  public focus() {
    return 1;
  }
}
// Parent component
import CommentForm from '@/components/comment-form.vue';

@Component({
  components: {CommentForm},
})

export default class Comments extends Vue {
  @Ref() readonly commentField!: CommentForm;

   public tryFocus() {
        this.commentField.focus(); // <- Error: TS2339: Property 'focus' does not exist on type 'Vue'.
    }
}
kaorun343 commented 5 years ago

@cawa-93 Hi.

This issue is caused by the type definition of *.vue file, and not caused by vue-property-decorator. *.vue files are defined to export Vue.

I recommend you to ask this question at Vue's official forum or its discord server.

Here is my solution. I usually separate component definition into .vue and .ts.

<script>
// CommentForm.vue
import CommentForm from './CommentForm'

export default CommentForm
</script>
// CommentForm.ts
@Component
export default class CommentForm extends Vue {
  public focus() {
    return 1;
  }
}
<script>
// Comments.vue
import Comments from './Comments'
import CommentForm from  '@/components/comment-form.vue'

export default Comments.extend({
  components: { CommentForm }
})
</script>
// Comments.ts
import CommentForm from '@/components/comment-form';

@Component
export default class Comments extends Vue {
  @Ref() readonly commentField!: CommentForm;

   public tryFocus() {
        this.commentField.focus(); // no error
    }
}
duola8789 commented 4 years ago

I also encountered this problem, do you have any solutions except separating component definition into .vue and .ts?

jyork03 commented 4 years ago

I'm also experiencing this issue and would like to find a solution that doesn't require separating the component definition into multiple files.

Given the example from @cawa-93 :

export default class Comments extends Vue {
  @Ref() readonly commentField!: CommentForm;

   public tryFocus() {
        this.commentField.focus(); // <- Error: TS2339: Property 'focus' does not exist on type 'Vue'.
    }
}

I would expect this.commentField to be recognized as type CommentForm, which extends Vue and has the method focus() defined, and not as it's ancestor type: Vue.

EDIT Excuse my ignorance, I'm just starting to learn typescript... Alright, I believe this behavior is a result of the declaration file shims-vue.d.ts. It causes all *.vue files to be exported as Vue types. @kaorun343 stated as much, but I (and possibly others) didn't quite understand exactly what he meant. The contents of the file are:

declare module '*.vue' {
  import Vue from 'vue';

  export default Vue;
}

Anyway, I've found a couple ways around it.

  1. Using // @ts-ignore. In my editor (Webstorm), this preserves Go To Definition functionality

    @Component
    export default class Comments extends Vue {
    @Ref() readonly commentField!: CommentForm;
    
    public tryFocus() {
        // @ts-ignore
        this.commentField.focus(); // no error
    }
    }
  2. Import it as any type. This breaks Go To Definition functionality

    @Component
    export default class Comments extends Vue {
    @Ref() readonly commentField!: any;
    
    public tryFocus() {
        this.commentField.focus(); // no error
    }
    }
  3. Create an interface for the component class and set the Ref to the interface type. This might be the typescript-purist way to handle this, but it also adds a lot of overhead.
    // components/types/index.ts
    export interface CommentFormInterface {
    focus: () => number;
    }
    // Child component
    @Component
    export default class CommentForm extends Vue implements CommentFormInterface {
    public focus() {
    return 1;
    }
    }
    
    import CommentForm from '@/components/comment-form.vue';
    import { CommentFormInterface } from '@/components/types';

@Component({ components: { CommentForm }, }) export default class Comments extends Vue { // @Ref() readonly commentField!: CommentFormInterface; // Go To Definition navigates to interface definition // or @Ref() readonly commentField!: CommentForm & CommentFormInterface; // Go To Definition navigates to CommentForm component definition.

public tryFocus() { this.commentField.focus(); // no error } }

4. Merge the type.  This might be repetitive, but it carries less boilerplate than option 3. It also breaks `Go To Definition`
```ts
@Component
export default class Comments extends Vue {
  @Ref() readonly commentField!: CommentForm & { focus: () => number };

   public tryFocus() {
        this.commentField.focus(); // no error
    }
}

As for me, I'll probably just go with option 1 and silence the issue with // @ts-ignore since this is an issue caused by the declaration in shims-vue.d.ts. Until something better comes along, and Vue components can export itself intact, everything will be a work-around anyways.

macfilho2 commented 2 years ago

I did handle this issue as following: Create a type with a property $el as a HTMLElement

export type VueComponent = { $el: HTMLElement }

And use solution 4 from @jyork03:

@Component
export default class Comments extends Vue {
  @Ref() readonly commentField!: CommentForm & VueComponent

   public tryFocus() {
        this.commentField.focus(); // no error
    }
}

Thus you don't need to write all the HTMLElement methods and properties