vue-class-component : TS2339 when calling class method
Asked Answered
S

3

3

I'm using vue-cli-service to build my vuejs application.

The build is successful, but in webstorm IDE, I get some TS2339 errors :

Test.vue:

<template>
    <div>{{method()}}</div>
</template>

<script lang="ts">
    import { Component, Vue } from 'vue-property-decorator';

    @Component
    export default class Test extends Vue {
        public method(): string {
            return 'hello';
        }
    }
</script>

Test.spec.ts:

import 'jest';
import {mount} from '@vue/test-utils';
import Test from '@/views/common/Test.vue';

describe('Test.vue', () => {
    let wrapper: any;

    beforeEach(() => {
        wrapper = mount(Test);
    });

    test('test method call', () => {
        const test = wrapper.find(Test).vm as Test;
        expect(test.method()).toEqual('hello');
    });
});

In Test.spec.ts, I get this error, both in editor and in typescript window:

Error:(14, 21) TS2339: Property 'method' does not exist on type 'Vue'.

But the test is OK, so test.method() is resolved successfully at runtime.

Sayyid answered 5/6, 2019 at 9:14 Comment(9)
Are you missing type definitions?Annamarieannamese
Shouldn't class definition be sufficient? What additional definition would you add here?Sayyid
What are you trying to achieve with 'const value = new Test().value'? It looks like you're trying to either inject a dependency or register the component locally, neither of which are done in the manner you have tried.Reduplicate
Of course, this example has no sense, this is just a simple example to show the problem. More generally, I have an class instance, and I want to get an attribute/call a method on this object.Sayyid
You'll save a lot of time if you provide a minimal example that shows what you're actually trying to achieve, rather than a nonsense example. Sounds like this may be a duplicate of: #46929213Reduplicate
I should have said the example is not useful but is has a sense. I create a "Test" object, why can't I access to the "value" attribute? Sorry if I can't post my real projet but in general a minimal example is what's asked to expose a problem. The link you posted does not seem to be related, is does not even mention TS2339 error.Sayyid
You're fixating on the TS2339 error, but are you sure that's what you actually need to fix? I can't think of any scenario where directly referencing an '@component' decorated class from another '@component' decorated class in the way you are suggesting is a useful construct. If you're confident your construct is valid, then by all means chase down the error code.Reduplicate
Also, AFAIK only Vue can instantiate components. You can't just new ComponentType().Annamarieannamese
OK, so I updated my code to show a more realistic case. The problem remains the same.Sayyid
S
1

Based on Steven's answer, I understood that shims-vue.d.ts is necessary to use component as typescript classes. But the problem is that they are all considered as Vue instances. This is obvious when looking at this file contents:

declare module '*.vue' {
  import Vue from 'vue';
  export default Vue;
}

For now, the only clean way I found is to declare an interface implemented by my component:

model.ts:

export interface ITest {
    method(): void;
}

Test.vue:

<template>
    <div>{{method()}}</div>
</template>

<script lang="ts">
    import { Component } from 'vue-property-decorator';
    import Vue from 'vue';
    import {ITest} from '@/views/test/model';

    @Component
    export default class Test extends Vue implements ITest {
        public method(): string {
            return 'hello';
        }
    }
</script>

Test.spec.ts:

import 'jest';
import {mount} from '@vue/test-utils';
import {ITest} from '@/views/test/model';
import Test from '@/views/test/Test.vue';

describe('Test.vue', () => {
    let wrapper: any;

    beforeEach(() => {
        wrapper = mount(Test);
    });

    test('test method call', () => {
        const test = wrapper.find(Test).vm as ITest;
        expect(test.method()).toEqual('hello');
    });
});
Sayyid answered 5/6, 2019 at 14:31 Comment(0)
S
1

Add these before your call.

// tslint:disable-next-line 
// @ts-ignore 

You can also union the existing Test interface like this:

const test = wrapper.find(Test).vm as Test & {method()};

Not to say you should do this in practice, but your code will run...

The correct way to do fix this is to augment Vue's definition so typescript will accept your method. But that should be happening automatically by Vue. Are you including the shims-vue.d.ts file. That's where the typescript magic happens?

https://v2.vuejs.org/v2/guide/typescript.html

With that said, I have had issues using the Vue class syntax and have had to revert to oldschool syntax to avoid typescript complaining:

<script lang="ts">
    import Vue from 'vue';

    export default Vue.extend({
        methods: {
          method(): string {
            return 'hello';
        }
    })
</script>

The shims file is how Vue augments itself with your components.

shims-vue.d.ts

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

Multiple Vue Instances

Sometimes Vue is included from multiple sources and this messes up typescript.

Try adding this to your tsconfig file.

{
    "paths": {
      "@/*": [
        "src/*"
      ],
      "vue/*": [
        "node_modules/vue/*"
      ]
}

I have sometimes even had to add webpack alias for this (this would be an issued building though, so not a fix for your issue):

        'vue$': path.resolve(__dirname, 'node_modules', 'vue/dist/vue.esm.js'),
Subversive answered 5/6, 2019 at 12:2 Comment(8)
Does not seem satisfying. I don't want to ignore error because I don't want test.unexistingMethod() to compile. About augmenting Vue's definition, it's not an option. Probably oldschool syntaxe would works, that makes me think it's a vue-class-component issue. I created an issue : github.com/vuejs/vue-class-component/issues/340Sayyid
I've got shims-vue.d.ts, but not sure it is used. What should I do with this file?Sayyid
Maybe your tsconfig file is excluding it.Subversive
It's not... but I'll look into that waySayyid
In fact, shims seems to declare all vue Components as Vue instances : github.com/vuejs/vue-class-component/issues/…Sayyid
shims is included, because if I remove the file, I get an error Error:(3, 18) TS2307: Cannot find module '@/views/crud/Test.vue'.Sayyid
I have seen this before when multiple Vue runtime were included. I had to hardcode a single path to vue in both webpack and typescript. I will add that to my solution.Subversive
Still not working. But seems normal according to the github link above?Sayyid
S
1

Based on Steven's answer, I understood that shims-vue.d.ts is necessary to use component as typescript classes. But the problem is that they are all considered as Vue instances. This is obvious when looking at this file contents:

declare module '*.vue' {
  import Vue from 'vue';
  export default Vue;
}

For now, the only clean way I found is to declare an interface implemented by my component:

model.ts:

export interface ITest {
    method(): void;
}

Test.vue:

<template>
    <div>{{method()}}</div>
</template>

<script lang="ts">
    import { Component } from 'vue-property-decorator';
    import Vue from 'vue';
    import {ITest} from '@/views/test/model';

    @Component
    export default class Test extends Vue implements ITest {
        public method(): string {
            return 'hello';
        }
    }
</script>

Test.spec.ts:

import 'jest';
import {mount} from '@vue/test-utils';
import {ITest} from '@/views/test/model';
import Test from '@/views/test/Test.vue';

describe('Test.vue', () => {
    let wrapper: any;

    beforeEach(() => {
        wrapper = mount(Test);
    });

    test('test method call', () => {
        const test = wrapper.find(Test).vm as ITest;
        expect(test.method()).toEqual('hello');
    });
});
Sayyid answered 5/6, 2019 at 14:31 Comment(0)
H
1

I noticed that Vue files are able to use other Vue files as their declared classes, which led me to try declaring the Jest files as Vue components as well. Surprisingly, it worked - no additional test-only interfaces required.

There are two steps. First, add the .vue suffix to Jest's test configuration in your package.json:

{
  "jest": {
    "testMatch": [
      "**/__tests__/**/*.test.ts",
      "**/__tests__/**/*.test.vue"
    ],
  }
}

Second, rename your test files to .test.vue and wrap them in a <script> block:

<script lang="ts">
import 'jest';

import { shallowMount } from '@vue/test-utils';

// Tests here...
</script>

Now you can use wrapper.vm as the actual declared component class type, and Vetur/Typescript will be completely happy in both, the IDE and the compiler output.

Hembree answered 19/12, 2019 at 1:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.