2
votes

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.

3
Are you missing type definitions?Devilscomrade
Shouldn't class definition be sufficient? What additional definition would you add here?jaudo
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.Ian Ash
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.jaudo
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: stackoverflow.com/questions/46928713/…Ian Ash

3 Answers

1
votes

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://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'),
1
votes

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');
    });
});
1
votes

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.