vue-loader icon indicating copy to clipboard operation
vue-loader copied to clipboard

Unable to use TypeScript type exports from vue component file

Open d-akara opened this issue 7 years ago • 25 comments

Version

15.0.0

Reproduction link

https://github.com/dakaraphi/vue-loader-bug-sample

Steps to reproduce

  1. clone referenced github repo.
  2. npm install
  3. npm run build

What is expected?

Should build without error

What is actually happening?

16:77-84 "export 'Message' was not found in './InlineMessage.vue'


This seems related to https://github.com/vuejs/vue-loader/issues/1234

d-akara avatar May 02 '18 16:05 d-akara

Vue files currently does not support type exports, because that's processed at the TypeScript checker level, and all *.vue modules are simply shimmed to be of type Vue (see your vue-shims.d.ts). I don't think this can be fixed in vue-loader unless we have first-class *.vue support in TS itself.

/cc @octref

yyx990803 avatar May 02 '18 20:05 yyx990803

@yyx990803 I forgot to mention, webpack will build properly on incremental build. So after the error is shown on initial build, if you then just touch the file InlineMessage.vue it will then build without error.

I don't understand enough of the internal build process to know if this is useful information or not, but just wanted to point it out in case.

d-akara avatar May 02 '18 21:05 d-akara

An unrelated opinion: I think we should put the below code in a separate ts file. because it's purely model mode.

export type MessageType = 'none' | 'info' | 'warning' | 'error' | 'success' | 'loading'
export interface Message {
     type: MessageType
     dismissable: boolean
     content: string
}

and I prefer declare a enum type for MessageType, such as:

export enum MessageType{
    none = 'none', 
    info = 'info' ,
    warning = 'warning',
    error = 'success' ,
    loading = 'loading',
}

codetalks-new avatar May 04 '18 06:05 codetalks-new

As @yyx990803 said there is no existing way to do this. Your best bet right now would be having a type.ts file where you define the types and import them from the Vue files.

@DanielRosenwasser Do you know what would it take to have this supported in TS? https://github.com/Microsoft/TypeScript/issues/12846 doesn't seem to cover it.

octref avatar May 14 '18 14:05 octref

Really, you need something special to resolve these types correctly within

  1. the language service
  2. the Webpack loaders themselves

There's always been a problem with importing from .vue files from a .ts file in the language service (i.e. in the editor)

I've forgotten whether the Webpack loaders are capable of doing "the right thing" without the vue-shims.d.ts file.

DanielRosenwasser avatar May 15 '18 22:05 DanielRosenwasser

Could this way help you ?

//index.ts

import Test from "./Test.vue";
export default Test;

You can just write your template and styles in .vue file (and it is surport scoped scss less) ,and script in ts files;

//Template.vue

<template>
  <div>Test</div>
</template>
<style lang="scss" scoped>
</style>

//test.ts

import { Vue, Component, Prop } from "vue-property-decorator";
import _Template from "./Template.vue";
@Component({
  mixins: [_Template],
})
export default class Test extends Vue {
  public id: string = "Test";
}

//in ohter .ts to import

import Test from "@/Test"
expport default TestChild extend Test{
console.info(this.id);
}

You can use ts type now; You can also use tsx like react; It is surport ts type;

@yyx990803 @dakaraphi

nailfar avatar May 16 '18 03:05 nailfar

@nailfar yes, it has been worked around using .ts files. It is just not as convenient.

d-akara avatar May 16 '18 14:05 d-akara

We can write a bash or node script to create module files.
run command such as npm run generate app/views/test. then create the Test.ts , Index.ts and Template.vue files with templates in app/views/test; // ts Template

import { Vue, Component, Prop } from "vue-property-decorator";
( other common import  you want here);
import _Template from "./Template.vue";
@Component({
  mixins: [_Template],
})
export default class {{FILE_NAME}} extends Vue {
  public id: string = "{{FILE_NAME}}";
  (do some thing others  you want...)
}

// vue Template

<template>
  <div class="module-{{FILE_NAME}}">{{FILE_NAME}}</div>
</template>
<style lang="scss" scoped>
</style>

// barrel Template

import {{FILE_NAME}} from "./{{FILE_NAME}}.vue";
export default {{FILE_NAME}};

{{FILE_NAME}} will be replaced by the script; one command many code;

nailfar avatar May 17 '18 01:05 nailfar

@nailfar This will make the hot update not work

ZSkycat avatar Aug 27 '18 05:08 ZSkycat

@ZSkycat https://github.com/skyrpex/vue-jsx-hot-loader

nailfar avatar Aug 27 '18 05:08 nailfar

This is the two methods I tried, but there are still problems. https://github.com/TypeStrong/ts-loader/issues/826

And a another way: Type conversion of vue components with additional ts files.

file list

Hello.vue // <script src="./Hello.script.ts"></script>
Hello.script.ts // <script> content
Hello.ts // magic wrap

Hello.ts content

import components from './Hello.vue';
import componentsClass from './Hello.script';
class Magic extends componentsClass {}
(<any>Magic) = components;
export default Magic;

This is written using vue-class-component.

ZSkycat avatar Aug 28 '18 19:08 ZSkycat

@yyx990803 It is probably a good idea to allow vue-loader to use the ts file as an entry point.

about webpack.module.rules We can use double extensions to avoid affecting normal ts files. For example *.vue.ts

(I have been exploring vue + typescript and if there is a chance I hope we can talk about it.)

ZSkycat avatar Aug 28 '18 19:08 ZSkycat

I found a more appropriate method. (https://github.com/TypeStrong/ts-loader/issues/826#issuecomment-416828492)

We only need to spoof the IDE's language service to achieve the goal. For example:

Demonstrates Repository: https://github.com/ZSkycat/issue-ts-loader-20180829/tree/better

Hello.vue

<template>
    <div><h1>Hello</h1></div>
</template>

<script src="./Hello.vue.ts"></script>

Hello.vue.ts

import { Component, Vue } from 'vue-property-decorator';

@Component<Hello>({
    name: 'Hello',
})
export default class Hello extends Vue {
    hello() {
        console.log('hello!');
    }
}

Used in ts file

import Hello from './Hello.vue';

The most important thing is the naming of the ts file.

  • For vscode, './Hello.vue' points to './Hello.vue.ts'
  • For webpack, './Hello.vue' points to './Hello.vue' and does not trigger type checking errors

ZSkycat avatar Aug 29 '18 05:08 ZSkycat

@dakaraphi

The real problem is that you should not reexport type here. Exporting type in vue file is fine.

More technical detail: Vue loader depends on ts-loader to transpile script. And ts-loader in turn requires vue-loader to extract script from vue file. Communication between two loaders are done via webpack: vue-loader will generate script files like app.vue.ts in webpack and ts-loader will read those files.

At first build, vue-loader hasn't generated script. To kick off the generation, vue-loader has to first ask ts-loader for script content to find out all components in the build. Thus, ts-loader transpiles script independently without other files' info in the first run because vue files aren't processed yet. But in later build type checking works because script generation has done before.

However, re-exporting type requires type info and cannot be transpiled solely by one single file.

My recommendation is to turn on --isolateModules flag in tsconfig, which bans re-exporting type and guarantees successful transpilation. Transpilation itself is essential in large app since it speeds up build, and it is also assumed by lots of other tools. For example, babel, thread-loader and fork-ts-checker-plugin all exploit TS's transpilation feature to some extent. isolateModules is also recommended officially. https://blogs.msdn.microsoft.com/typescript/2018/08/27/typescript-and-babel-7/

As @yyx990803 said, vue-loader cannot do much here. So this issue can be safely closed since exporting type actually works . Supporting re-exporting type isn't widely supported in JS community and I suspect its value.

HerringtonDarkholme avatar Aug 29 '18 05:08 HerringtonDarkholme

There is a workaround with default objects. // types.ts

// Type intended to share between vue components
type MyCommonType = {
    id?:string,
    name:string,
}

export const MyDefaultProp:MyCommonType = {
    id:'',
    name:'',
}

// component1.vue

. . .
import {MyDefaultProp} from './types.ts'

type MyPropType = typeof MyDefaultProp // Better than defining whole type/interface

export default class Component1 extends Vue {
    @Prop({ default: () => Object.assign({}, MyDefaultProp), type: Object }) myProp!: MyPropType
. . . 

// component2.vue

. . .
import {MyDefaultProp} from './types.ts'

type MyPropType = typeof MyDefaultProp // Better than defining whole type/interface

export default class Component2 extends Vue {
    @Prop({ default: () => Object.assign({}, MyDefaultProp), type: Object }) myProp!: MyPropType
. . . 

Typescript compiler will be happy without any warning...

reshma-menon avatar Sep 11 '18 08:09 reshma-menon

I am gonna import ts file in vue component. But the following error happens.

marvelperseus avatar Jan 07 '19 16:01 marvelperseus

I found a more appropriate method. (TypeStrong/ts-loader#826 (comment))

We only need to spoof the IDE's language service to achieve the goal. For example:

Demonstrates Repository: https://github.com/ZSkycat/issue-ts-loader-20180829/tree/better

Hello.vue

<template>
    <div><h1>Hello</h1></div>
</template>

<script src="./Hello.vue.ts"></script>

Hello.vue.ts

import { Component, Vue } from 'vue-property-decorator';

@Component<Hello>({
    name: 'Hello',
})
export default class Hello extends Vue {
    hello() {
        console.log('hello!');
    }
}

Used in ts file

import Hello from './Hello.vue';

The most important thing is the naming of the ts file.

  • For vscode, './Hello.vue' points to './Hello.vue.ts'
  • For webpack, './Hello.vue' points to './Hello.vue' and does not trigger type checking errors

Is there some problems with this solution ? I tried it on some components seems ok but why this is not preferred way then ?

renetik avatar Jun 08 '19 02:06 renetik

@rene-dohan I would say doing it that way defeats the purpose of single file components. It's fine if you want to split it your code to work around this issue but many do not. A separate type definitions file is much more palatable as that is pretty common in Typescript land.

jaredmcateer avatar Jun 26 '19 21:06 jaredmcateer

这问题有任何进展吗?或者在将来的vue 3中是否能解决,如果不,那么vue,sfc文件的地位可能不保,按照composition api和jsx的方式开展开发,可能是vue 3的最佳方向。但这意味着,这跟react已经很贴近了。 现在我正在寻找scoped css在ts的可行方案,这路线感觉比ts支持vue更靠谱

harvey-woo avatar Mar 09 '20 04:03 harvey-woo

@rene-dohan Is there some problems with this solution ? I tried it on some components seems ok but why this is not preferred way then ?

Maybe you came from React or Angular world, but I cannot imagine having components separated into many different files in my project. For me Vuejs is the best because just one single file component and a proper HTML, not tsx ugly monster. So I can focus on bringing new features instead of messing around with huge amount of different files and parts and types.

I have a working solution but sometimes it is not working as expected in bigger projects. I think if we try to push this solution to vuejs/webpack/typescript developers, it might end our pain.

MyComponent.vue:

<template>
    <div>{{ test }}</div>
</template>

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

    export default Vue.extend({
        data() {
            return {
                test: "abc"
            };
        },
        methods: {
            reverse(): void {
                this.test = "cba";
            }
        }
    });
</script>

<style lang="scss" scoped>
    div { background: $primaryBgColor; }
</style>

AnotherComponent.vue:

<template>
    <div>
        <MyComponent ref="myComponent" />
        <input type="button" value="Click me" @click="onClick">
    </div>
</template>

<script lang="ts">
    import Vue from "vue";
    import MyComponent from "./MyComponent.vue";
    type MyComponentType = InstanceType<typeof MyComponent>;

    export default Vue.extend({
        components: {
            MyComponent
        },
        data() {
            return {
            };
        },
        methods: {
            onClick(): void {
                (this.$refs.myComponent as MyComponentType).reverse(); // Compiling with no errors
                (this.$refs.myComponent as MyComponentType).reverse2(); // Error: 'reverse2' does not exist
            }
        }
    });
</script>

You can find the working sample here: https://github.com/fairking/vuetstest

What do you think guys?

fairking avatar Dec 21 '21 22:12 fairking

Interestingly, in my testing with the latest Vue CLI, I can indeed export a type from a .vue file and import it into another .vue file. But I cannot export a type from a .vue file and import it into a .ts file without an error. Any insight as to why this might be?

My current "workaround" is to just have the types in an accompanying .d.ts file alongside the .vue file that needs it. But that adds a lot of unnecessary files.

vincerubinetti avatar Feb 23 '22 23:02 vincerubinetti

Interestingly, in my testing with the latest Vue CLI, I can indeed export a type from a .vue file and import it into another .vue file. But I cannot export a type from a .vue file and import it into a .ts file without an error. Any insight as to why this might be?

My current "workaround" is to just have the types in an accompanying .d.ts file alongside the .vue file that needs it. But that adds a lot of unnecessary files.

@vincerubinetti This is because the Vue tooling is aware of the Vue SFCs, but the Typescript tooling is not, which is why each project has a vue-shim.d.ts or similar that types the default export of any file matching the *.vue pattern to VueConstructor<Vue>. As you discovered having a supplementary .d.ts or simply exporting common types from a separate .ts file is the current workaround for the issue.

jaredmcateer avatar Feb 24 '22 15:02 jaredmcateer

I would love to see this get resolved, otherwise I have to keep some component members (types, enums) in a separate file somewhere else. Certainly not the biggest issue in the world, but results in degraded code organization.

fabis94 avatar Apr 28 '22 14:04 fabis94

Hi, was wondering if there is a common suffix that has been converged on, for the "code behind" file that is needed for this. i.e., Foo.vue is commonly coupled with a Foo_xyz.ts file, or similar? I had been using the "obvious" thing of Foo.vue and Foo.ts but then I ran into jest module import problems (resolving /Foo to /Foo.vue, instead of /Foo.ts) and it made me wonder what others are naming their files.

softwareCobbler avatar Aug 03 '22 21:08 softwareCobbler

I came here looking for solution, albeit I'm using Vite, not Webpack.

In the case someone comes with the same question, here's the solution:

  • vue -> ts type imports work with Vite well, nothing to do there
  • To fix VSCode showing errors, you should use Volar's Takeover mode(i.e. just disable buildin TS extension for workspace) to use Vue-compatible TS parser

Dimava avatar Apr 22 '23 06:04 Dimava