Vue&TypeScript:在项目目录之外的TypeScript组件中实施导入时,如何避免错误TS2345?

时间:2019-08-05 09:10:22

标签: typescript vue.js vue-component typescript-decorator typescript-definitions

当尝试使用辅助组件(项目目录外部)时,出现TypeScript错误:

TS2345: Argument of type '{ template: string; components: { SimpleCheckbox: typeof SimpleCheckbox; }; }' 
is not assignable to parameter of type 'VueClass<Vue>'.
Object literal may only specify known properties, and 'template' does not exist in type 
'VueClass<Vue>'.

我的WebStorm IDE未检测到此错误;当我使用TypeScript加载程序运行Webpack时,在控制台中输出了in。

错误发生在:

import { Vue, Component, Prop } from 'vue-property-decorator';
import template from './SkipProjectInitializationStepPanel.pug';
import SimpleCheckbox from './../../../../ui-kit-libary-in-development/UiComponents/Checkboxes/MaterialDesign/SimpleCheckbox.vue';


@Component({ template, components: { SimpleCheckbox } }) // here !
export default class SkipProjectInitializationStepPanel extends Vue {
  @Prop({ type: String, required: true }) private readonly text!: string;
}

根据ui-kit-libary-in-development的名称,这还不是npm-dependency,因此暂时不在node_modules内。

这完全是TypeScript错误;尽管ts-loader会引发此错误,但是Webpack会构建我的项目,并且编译的JavaScript可以正常工作。如果执行以下操作之一,此错误将消失:

  • SimpleCheckbox.vueSkipProjectInitializationStepPanel.ts移至同一目录并将其导入为import SimpleCheckbox from './SimpleCheckbox.vue';
  • SimpleCheckbox中删除@Component({ template, components: { SimpleCheckbox } })并仅保留@Component({ template, components: {} })(当然,在这种情况下不会渲染SimpleCheckbox,但事实证明问题不在{{ 1}})。
  • SkipProjectInitializationStepPanel移动到主项目的ui-kit-libary-in-development并从node_modules中删除node_modules(如果不删除,则不会有任何改变)。

很遗憾,我无法重现此问题。出于某些原因,下面的再现尝试可以正确执行:

MainProject / src / Application.vue

ui-kit-libary-in-development

MainProject / src / PageComponents / PageOne.ts

<template lang="pug">
  PageOne
</template>

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

  @Component({ components: { PageOne }})
  export default class Application extends Vue {
    private created(): void {
      console.log('Done.');
    }
  }
</script>

MainProject / src / PageComponents / PageOne.pug

import { Vue, Component, Prop } from 'vue-property-decorator';
import template from './PageOne.pug';
import Button from './../../../UiKitLibraryStillInDevelopment/UiComponents/Buttons/Button.vue';


@Component({ template, components: { Button } })
export default class SkipProjectInitializationStepPanel extends Vue {}

ui-kit-libary-in-development / UiComponents / Buttons / Button.vue

.RootElement
  Button(:text="'Click me'")

我发现的所有线索都是问题Setting components in Component decorator causes Typescript 2.4 error中的评论:

  

侧面组件应添加.d.ts才能使用AFAIK。

     

Nick Messing

根据此线索,出现了以下问题:

  • 在主项目或依赖项中应在哪里创建<template lang="pug"> button {{ text }} </template> <script lang="ts"> import { Vue, Component, Prop } from 'vue-property-decorator'; @Component export default class SimpleCheckbox extends Vue { @Prop({ type: String, required: true }) private readonly text!: string; private created(): void { console.log('OK!'); console.log(this.$props); } } </script> ?最有可能在主项目中,但是如果是这样,为什么我可以在.d.ts之类的第三方库中导入辅助组件?因为那里有vuetify
  • 我如何需要在.d.ts中声明新的Vue组件?一些教程或示例?

赏金源文件

由于我无法重现此问题,并且我的项目仍是原始项目(尚未获得商业价值),因此可以通过Google云端硬盘(link for downloading zip archive)进行共享。包括所有.d.ts,只需在node_modules目录中运行npm run developmentBuild

如果您担心潜在的病毒,也可以在this repository中获取源文件,但是由于它不包含main-project,因此为了重现,在两个目录中都必须执行node_modules npm installmain-project目录。

2 个答案:

答案 0 :(得分:5)

这已经是一个很长的答案。如果您没有时间阅读所有内容,请在结尾处输入 TL; DR


分析

错误消息

起初,我并不真正理解为什么TypeScript提到VueClass<Vue>并抱怨template属性。但是,当我查看Component装饰器的类型定义时,情况变得更加清楚了:

vue-class-component/lib/index.d.ts(部分省略)

declare function Component<V extends Vue>(options: ComponentOptions<V> & ThisType<V>): <VC extends VueClass<V>>(target: VC) => VC;
// ...
declare function Component<VC extends VueClass<Vue>>(target: VC): VC;

在这里我们可以看到Component有两个签名。第一个像您一样使用,第二个不带选项(@Component class Foo)。

显然,编译器认为我们的用法与第一个签名不匹配,因此它必须是第二个签名。因此,我们最终得到一条错误消息,内容为VueClass<Vue>

  

注意::在最新版本(3.6.3)中,TypeScript实际上会显示出更好的错误,说明重载和不匹配的原因。

更好的错误消息

我接下来要做的是暂时注释掉main-project/node_modules/vue-class-component中的第二个函数声明,并确保我们得到了不同的错误消息。新消息跨越63行,因此我认为将它作为一个整体包含在这篇文章中是没有意义的。

ERROR in /tmp/main-project/InitializeProjectGUI__assets/SingletonComponents/SkipProjectInitializationStepPanel/SkipProjectInitializationStepPanel.ts
../InitializeProjectGUI__assets/SingletonComponents/SkipProjectInitializationStepPanel/SkipProjectInitializationStepPanel.ts
[tsl] ERROR in /tmp/main-project/InitializeProjectGUI__assets/SingletonComponents/SkipProjectInitializationStepPanel/SkipProjectInitializationStepPanel.ts(10,24)
      TS2322: Type '{ SimpleCheckbox: typeof SimpleCheckbox; }' is not assignable to type '{ [key: string]: VueConstructor<Vue> | FunctionalComponentOptions<any, PropsDefinition<any>> | ComponentOptions<never, any, any, any, any, Record<string, any>> | AsyncComponentPromise<any, any, any, any> | AsyncComponentFactory<...>; }'.
  Property 'SimpleCheckbox' is incompatible with index signature.
    Type 'typeof SimpleCheckbox' is not assignable to type 'VueConstructor<Vue> | FunctionalComponentOptions<any, PropsDefinition<any>> | ComponentOptions<never, any, any, any, any, Record<string, any>> | AsyncComponentPromise<any, any, any, any> | AsyncComponentFactory<...>'.
      Type 'typeof SimpleCheckbox' is not assignable to type 'VueConstructor<Vue>'.
        Types of property 'extend' are incompatible.
          Type '{ <Data, Methods, Computed, PropNames extends string = never>(options?: import("/tmp/dependency/node_modules/vue/types/options").ThisTypedComponentOptionsWithArrayProps<import("/tmp/depende...' is not assignable to type '{ <Data, Methods, Computed, PropNames extends string = never>(options?: import("/tmp/main-project/node_modules/vue/types/options").ThisTypedComponentOptionsWithArrayProps<import("/tmp/main-...'.
            ...
              Type 'import("/tmp/dependency/node_modules/vue/types/vnode").ScopedSlotReturnValue' is not assignable to type 'import("/tmp/main-project/node_modules/vue/types/vnode").ScopedSlotReturnValue'.
                Type 'VNode' is not assignable to type 'ScopedSlotReturnValue'.

如您所见,该错误消息非常难以阅读,并且并未真正指出特定问题。因此,相反,我着手开始降低项目的复杂性,以便更好地理解问题。

最小示例

我将为您省去涉及很多试验和错误的整个过程。让我们直接跳转到结果。

结构

├─ main-project
│  ├─ node_modules // installed packages listed below (without sub-dependencies)
│  │     ts-loader@6.1.0
│  │     typescript@3.6.3
│  │     vue@2.6.10
│  │     vue-property-decorator@8.2.2
│  │     vuex@3.1.1
│  │     webpack@4.40.2
│  │     webpack-cli@3.3.9
│  ├─ SkipProjectInitializationStepPanel.ts
│  ├─ tsconfig.json
│  └─ webpack.config.js
└─ dependency
   ├─ node_modules // installed packages listed below (without sub-dependencies)
   │     vue@2.6.10
   └─ SimpleCheckbox.ts

main-project/tsconfig.json

{
  "compilerOptions": {
    "target": "es6",
    "strict": true,
    "moduleResolution": "node"
  }
}

main-project/webpack.config.js

module.exports = {
    entry: './SkipProjectInitializationStepPanel.ts',
    mode: 'development',
    module: {
        rules: [
            {
                test: /\.ts$/,
                loader: 'ts-loader'
            }
        ]
    },
    resolve: {
        extensions: ['.ts', '.js']
    }
};

main-project/SkipProjectInitializationStepPanel.ts

import { Component } from 'vue-property-decorator';
import 'vuex';

import SimpleCheckbox from '../dependency/SimpleCheckbox';

Component({ template: '', components: { SimpleCheckbox } });

dependency/SimpleCheckbox.ts

import Vue from 'vue';

export default class SimpleCheckbox extends Vue {}

main-projectnpm run webpack运行此示例时,我们得到与以前完全相同的错误。任务完成了。

发生了什么事

在我删除项目的一部分以简化为这个最小的示例时,我学到了一些非常有趣的东西。

您可能已经注意到我添加到import 'vuex'的{​​{1}}。在原始项目中,SkipProjectInitializationStepPanel.ts当然是从不同的地方(例如vuex)导入的,但这对于重现该问题并不重要。关键部分是 main-project/Source/ProjectInitializer/Store/Store.ts会导入main-project,而vuex不会

要了解为什么导入dependency会导致此问题,我们必须查看vuex的类型定义。

vuex/types/index.d.ts(前几行)

vuex

在这里,我们对第二个import _Vue, { WatchOptions } from "vue"; // augment typings of Vue.js import "./vue"; import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from "./helpers"; 感兴趣。该评论实际上已经给了我们提示: Vue.js的增强类型

vuex/types/vue.d.ts(部分省略)

import

这是罪魁祸首。 import { Store } from "./index"; // ... declare module "vue/types/vue" { interface Vue { $store: Store<any>; } } 类型利用declaration mergingvuex添加到$store的{​​{1}}接口中。看来,这种扩充仅发生在与Vue相同的vue中的“本地”类型上。由于node_modules具有增强的vuex,而main-project具有原始的,因此两者不匹配。

TS加载程序

在我进行的所有测试中,仅凭Vue就无法重现问题。显然dependency所做的动作与导致此问题的方式有所不同。它可能与使用Webpack进行模块解析有关,也可能完全不同。我不知道。

解决方法

我有一些想法可以解决或解决此问题。尽管这些不一定是立即可用的解决方案,而是不同的方法和想法。

tsc添加到ts-loader

由于实际上并不是从vuex中删除dependency的原因,因此我们唯一要做的就是使两个vuex接口都匹配,而在{{ 1}}。

奇怪的是,我能够在最小的示例中获得此修复,但在原始项目中却没有。我还不知道为什么会这样。

除此之外,它不是很优雅,您可能必须从main-project引用的每个文件中导入Vue

使用共享的vuex

拥有共享的dependency文件夹意味着两个项目使用相同的vuex,因此该问题就消失了。根据您的要求,以两个项目共享相同的main-project文件夹的方式组织两个项目可能是一个不错的解决方案。您可能还想看看诸如LernaYarn workspaces之类的工具,可以为您提供帮助。

node_modules打包使用

您说node_modules 还不是[an] npm依赖项。也许是时候做到这一点了。与共享的vue目录类似,这将导致两个项目使用相同的node_modules安装,这将解决此问题。

进一步调查

如前所述,只有dependency会发生这种情况。 也许可以在dependency中进行某些固定或配置,以避免出现此问题。

TL; DR

node_modules导入vue,而ts-loader不导入。 ts-loader通过使用declaration merging向其添加main-project属性来扩展vuex接口。现在,一个项目中的dependency与其他项目中的vuex不匹配,从而导致错误。

答案 1 :(得分:0)

如@lukasgeiter所说,

  

显然ts-loader的行为有所不同,导致了此问题。

如果将类型检查过程委托给fork-ts-checker-webpack-plugin,错误将消失。