Vue&TypeScript:在项目目录外的TypeScript组件中实现导入时如何避免错误TS2345? [英] Vue&TypeScript: how to avoid error TS2345 when import implemented in TypeScript component outside of project directory?
问题描述
尝试使用侧组件(在项目目录之外)时出现以下 TypeScript 错误:
TS2345:类型'{模板:字符串;组件:{ SimpleCheckbox: typeof SimpleCheckbox;};}'不可分配给VueClass"类型的参数.对象字面量只能指定已知属性,类型中不存在模板"'VueClass'.
我的 WebStorm IDE 没有检测到这个错误;当我使用 TypeScript 加载器运行 Webpack 时,在控制台中输出了 in.
错误发生在:
import { Vue, Component, Prop } from 'vue-property-decorator';从'./SkipProjectInitializationStepPanel.pug'导入模板;从'./../../../../ui-kit-libary-in-development/UiComponents/Checkboxes/MaterialDesign/SimpleCheckbox.vue'导入SimpleCheckbox;@Component({ template, components: { SimpleCheckbox } })//这里!导出默认类 SkipProjectInitializationStepPanel 扩展 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.vue
移动到与SkipProjectInitializationStepPanel.ts
相同的目录,并将其导入为import SimpleCheckbox from './SimpleCheckbox.vue';
- 从
@Component({ template, components: { SimpleCheckbox } })
中移除SimpleCheckbox
并只留下@Component({ template, components: {} })
(当然,在这种情况下SimpleCheckbox
不会被渲染,但证明问题不在SkipProjectInitializationStepPanel
). - 将
ui-kit-libary-in-development
移动到主项目的node_modules
并从ui-kit- 中删除
(如果不删除,什么都不会改变).node_modules
libary-in-development
很遗憾,我无法重现此问题.由于某种原因,下面的复制尝试没有错误:
MainProject/src/Application.vue
第一页</模板><script lang="ts">从vue-property-decorator"导入 { Vue, Component };从 './PageComponents/PageOne' 导入 PageOne@Component({ 组件: { PageOne }})导出默认类应用程序扩展 Vue {私人创建():无效{console.log('完成.');}}
MainProject/src/PageComponents/PageOne.ts
import { Vue, Component, Prop } from 'vue-property-decorator';从'./PageOne.pug'导入模板;从'./../../../UiKitLibraryStillInDevelopment/UiComponents/Buttons/Button.vue'导入按钮;@Component({ 模板,组件:{ 按钮 } })导出默认类 SkipProjectInitializationStepPanel 扩展 Vue {}
MainProject/src/PageComponents/PageOne.pug
.RootElementButton(:text="'点击我'")
ui-kit-libary-in-development/UiComponents/Buttons/Button.vue
按钮 {{ 文字 }}</模板><script lang="ts">从vue-property-decorator"导入 { Vue, Component, Prop };@成分导出默认类 SimpleCheckbox 扩展 Vue {@Prop({ type: String, required: true }) private readonly text!: string;私人创建():无效{console.log('好的!');console.log(this.$props);}}
我发现的所有线索都是这个问题的评论 在组件装饰器中设置组件导致 Typescript 2.4 错误:
<块引用>Side 组件应该添加 .d.ts 以使其在 AFAIK 中工作.
从这个线索,产生以下问题:
- 我应该在哪里创建
.d.ts
- 在我的主项目或依赖项中?最有可能在主项目中,但如果是这样,为什么我可以在像vuetify
这样的第三方库中导入侧组件?因为那里有.d.ts
! - 我需要如何在
.d.ts
中声明新的 Vue 组件?一些教程或示例?
赏金的源文件
因为我无法重现这个问题,而且我的项目仍然是原始的(还没有商业价值),我可以通过 Google Drive 分享它(下载 zip 存档的链接).包含所有node_modules
,只需在main-project
目录下运行npm run developmentBuild
即可.
如果你担心潜在的病毒,你也可以在这个仓库中获取源文件,但因为它不包含 node_modules
,为了重现它需要在 main-project
和 dependency<中执行
npm install
/code> 目录.
这已经成为一个很长的答案.如果您没有时间阅读全部内容,请查看TL;末尾的 DR.
分析
错误信息
起初我真的不明白为什么 TypeScript 提到 VueClass
并抱怨 template
属性.然而,当我查看 Component
装饰器的类型定义时,事情变得更清楚了:
vue-class-component/lib/index.d.ts
(部分省略)
声明函数Component(options: ComponentOptions & ThisType): >(目标:VC) =>VC;//...声明函数 Component>(target: VC): VC;
我们在这里可以看到 Component
有两个签名.第一个像你一样使用,第二个没有选项(@Component class Foo
).
显然编译器认为我们的用法与第一个签名不匹配,所以它必须是第二个.因此,我们以 VueClass
结束了错误消息.
注意:在最新版本 (3.6.3) 中,TypeScript 实际上会显示更好的错误,说明两个重载以及它们不匹配的原因.
更好的错误信息
接下来我做的是暂时注释掉main-project/node_modules/vue-class-component
中的第二个函数声明,果然我们得到了不同的错误信息.新消息跨越 63 行,所以我认为将它作为一个整体包含在这篇文章中是没有意义的.
错误在/tmp/main-project/InitializeProjectGUI__assets/SingletonComponents/SkipProjectInitializationStepPanel/SkipProjectInitializationStepPanel.ts../InitializeProjectGUI__assets/SingletonComponents/SkipProjectInitializationStepPanel/SkipProjectInitializationStepPanel.ts[tsl]/tmp/main-project/InitializeProjectGUI__assets/SingletonComponents/SkipProjectInitializationStepPanel/SkipProjectInitializationStepPanel.ts(10,24) 中的错误TS2322: 类型 '{ SimpleCheckbox: typeof SimpleCheckbox;}' 不可分配给类型 '{ [key: string]: VueConstructor<Vue>|FunctionalComponentOptions>|ComponentOptions>|AsyncComponentPromise|AsyncComponentFactory<...>;}'.属性SimpleCheckbox"与索引签名不兼容.类型typeof SimpleCheckbox"不可分配给类型VueConstructor<Vue>|FunctionalComponentOptions>|ComponentOptions>|AsyncComponentPromise|AsyncComponentFactory<...>'.类型typeof SimpleCheckbox"不可分配给类型VueConstructor".属性扩展"的类型不兼容.Type '{ (options?: import("/tmp/dependency/node_modules/vue/types/options").ThisTypedComponentOptionsWithArrayProps(options?: import("/tmp/main-project/node_modules/vue/types/options").ThisTypedComponentOptionsWithArrayProps<import("/tmp/main-...'....类型 'import("/tmp/dependency/node_modules/vue/types/vnode").ScopedSlotReturnValue' 不可分配给类型 'import("/tmp/main-project/node_modules/vue/types/vnode").ScopedSlotReturnValue'.类型VNode"不可分配给类型ScopedSlotReturnValue".
如您所见,错误消息很难阅读,并且并未真正指向特定问题.因此,我继续并开始降低项目的复杂性,以便更好地理解问题.
最小示例
我会为您省去涉及大量反复试验的整个过程.让我们直接跳到结果.
结构
├─主项目│ ├─ node_modules//下面列出的已安装包(无子依赖)│ │ ts-loader@6.1.0│ │ 打字稿@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└─ 依赖├─ node_modules//下面列出的已安装包(无子依赖)│ vue@2.6.10└─ SimpleCheckbox.ts
main-project/tsconfig.json
<代码>{编译器选项":{目标":es6",严格":真实,模块分辨率":节点";}}
main-project/webpack.config.js
:
module.exports = {条目:'./SkipProjectInitializationStepPanel.ts',模式:'发展',模块: {规则: [{测试:/\.ts$/,装载机:'ts-装载机'}]},解决: {扩展名:['.ts', '.js']}};
main-project/SkipProjectInitializationStepPanel.ts
:
import { Component } from 'vue-property-decorator';导入'vuex';从 '../dependency/SimpleCheckbox' 导入 SimpleCheckbox;组件({模板:'',组件:{SimpleCheckbox}});
dependency/SimpleCheckbox.ts
:
从'vue'导入Vue;导出默认类 SimpleCheckbox 扩展 Vue {}
当从 main-project
和 npm run webpack
运行这个例子时,我们得到与之前完全相同的错误.任务完成.
发生了什么?
当我删除项目的一部分以将其归结为这个最小示例时,我学到了一些非常有趣的东西.
您可能已经注意到我添加到 SkipProjectInitializationStepPanel.ts
的 import 'vuex'
.在原始项目中 vuex
当然是从不同地方导入的(例如 main-project/Source/ProjectInitializer/Store/Store.ts
)但这对于重现问题并不重要.关键是 main-project
导入 vuex
而 dependency
没有.
要找出导入vuex
导致这个问题的原因,我们必须查看vuex
的类型定义.
import _Vue, { WatchOptions } from "vue";//增加 Vue.js 的类型导入./vue";import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from "./helpers";
这里我们对第二个 import
感兴趣.评论实际上已经给了我们一个提示:增强 Vue.js 的类型.
vuex/types/vue.d.ts
(部分省略)
import { Store } from "./index";//...声明模块vue/types/vue"{界面 Vue {$store: Store;}}
这就是罪魁祸首.vuex
类型使用 声明合并在vue
的Vue
接口中添加$store
.似乎这种增强只发生在本地"输入与 vuex
相同的 node_modules
.因为main-project
有增强的Vue
,dependency
有原始的,两者不匹配.
TS 加载器
在我所有的测试中,我无法仅使用 tsc
重现该问题.显然 ts-loader
做了一些不同的事情,导致了这个问题.它可能与使用 Webpack 进行模块解析有关,也可能完全不同.我不知道.
解决方法
我对如何解决或解决此问题有一些想法.尽管这些不一定是现成的解决方案,而是不同的方法和想法.
将 vuex
添加到 dependency
因为从 main-project
中删除 vuex
并不是一个真正的选择,我们唯一能做的就是让两个 Vue
接口匹配, 也包括 vuex
在 dependency
中.
这里的奇怪之处在于,我能够在我的最小示例中使用此修复程序,但在原始项目中却无法使用.我还没弄清楚为什么会这样.
除此之外,它不是很优雅,您可能必须从 main-project
引用的每个文件中导入 vuex
.
使用共享的node_modules
共享node_modules
文件夹意味着两个项目使用相同的vue
,所以这个问题就消失了.根据您的要求,以共享相同 node_modules
文件夹的方式组织这两个项目可能是一个很好的解决方案.您可能还想看看诸如 Lerna 或 Yarn 工作区 可以帮助解决这个问题.
将 dependency
作为一个包使用
你说dependency
还不是[an] npm-dependency.也许是时候让它成为一个了.与共享 node_modules
目录类似,这将导致两个项目使用相同的 vue
安装,这应该可以解决问题.
进一步调查
如前所述,这只发生在 ts-loader
上.也许可以在 ts-loader
中修复或配置一些东西来避免这个问题.
TL;DR
main-project
导入 vuex
而 dependency
不导入.vuex
使用 Vue 接口> 声明合并 向其添加 $store
属性.现在,一个项目的 Vue
与其他项目的 Vue
不匹配,导致错误.
I got below TypeScript error when tried to use side component (outside of project directory):
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>'.
My WebStorm IDE did not detect this error; in was outputted in console when I ran Webpack with TypeScript loader.
The error occurs 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;
}
As follows from ui-kit-libary-in-development
name, this is not npm-dependency yet, so it is not inside node_modules
for now.
It was exclusively TypeScript error; although ts-loader
casts this error, Webpack builds my project and compiled JavaScript works correctly. This error will disappear if to do one of below actions:
- Move
SimpleCheckbox.vue
to same directory asSkipProjectInitializationStepPanel.ts
and import it asimport SimpleCheckbox from './SimpleCheckbox.vue';
- Remove
SimpleCheckbox
from@Component({ template, components: { SimpleCheckbox } })
and leave only@Component({ template, components: {} })
(off course,SimpleCheckbox
will no be rendered in this case, but it proves that problem is not inSkipProjectInitializationStepPanel
). - Move
ui-kit-libary-in-development
tonode_modules
of main project and removenode_modules
fromui-kit-libary-in-development
(if don't remove, nothing will change).
Unfortunately, I could not reproduce this problem. For some reason below try of reproduction works without errors:
MainProject/src/Application.vue
<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.ts
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 {}
MainProject/src/PageComponents/PageOne.pug
.RootElement
Button(:text="'Click me'")
ui-kit-libary-in-development/UiComponents/Buttons/Button.vue
<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>
All clues what I found is this comment in issue Setting components in Component decorator causes Typescript 2.4 error:
Side components should add .d.ts for it to work AFAIK.
From this clue, the following question arising:
- Where I should to create
.d.ts
- in my main project or dependency? Most likely in main project, but if it so, why I can import side components in third-party libraries likevuetify
? Because there is.d.ts
there! - How I need to declare new Vue component in
.d.ts
? Some tutorial or example?
Source files for bounty
Because I could not reproduce this problem and my project still is raw (has not got commercial value yet), I can share it by Google Drive (link for downloading zip archive). All node_modules
are included, just run npm run developmentBuild
in main-project
directory.
If you are worry about potential viruses, you can also get source files in this repository, but because it is does not include node_modules
, for reproducing it's required to execute npm install
in both main-project
and dependency
directories.
This has become quite a long answer. If you don't have time to read it all there's a TL;DR at the end.
Analysis
Error Message
At first I didn't really understand why TypeScript mentioning VueClass<Vue>
and complaining about the template
property. However, when I looked at the type definitions for the Component
decorator, things became a bit clearer:
vue-class-component/lib/index.d.ts
(parts omitted)
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;
What we can see here is that Component
has two signatures. The first one to be used like you did, and the second one without options (@Component class Foo
).
Apparently the compiler thinks our usage doesn't match the first signature so it must be the second one. Therefore we end up with an error message with VueClass<Vue>
.
Note: In the latest version (3.6.3), TypeScript will actually display a better error, stating both overloads and why they don't match.
Better Error Message
The next thing I did was temporarily comment out the second function declaration in main-project/node_modules/vue-class-component
and sure enough we get a different error message. The new message spans 63 lines so I figured it wouldn't make sense to include it in this post as a whole.
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'.
As you can see the error message is quite hard to read and doesn't really point to a specific problem. So instead I went ahead and started reducing the complexity of the project in order to understand the issue better.
Minimal Example
I'll spare you the whole process which involved lots of trial and error. Let's jump directly to the result.
Structure
├─ 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 {}
When running this example from main-project
with npm run webpack
, we get the exact same error as before. Mission accomplished.
What's Happening?
While I was removing parts of the project to get it down to this minimal example, I learned something very interesting.
You might have noticed the import 'vuex'
I've added to SkipProjectInitializationStepPanel.ts
. In the original project vuex
is of course imported from different places (e.g. main-project/Source/ProjectInitializer/Store/Store.ts
) but that's not important for reproducing the issue. The crucial part is that main-project
imports vuex
and dependency
doesn't.
To find out why importing vuex
causes this issue, we have to look at the type definitions of vuex
.
vuex/types/index.d.ts
(first few lines)
import _Vue, { WatchOptions } from "vue";
// augment typings of Vue.js
import "./vue";
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from "./helpers";
Here we are interested in the second import
. The comment actually gives us a hint already: augment typings of Vue.js.
vuex/types/vue.d.ts
(parts omitted)
import { Store } from "./index";
// ...
declare module "vue/types/vue" {
interface Vue {
$store: Store<any>;
}
}
And here we have the culprit. The vuex
types make use of declaration merging to add $store
to the Vue
interface of vue
. It seems that this augmentation only happens to "local" types in the same node_modules
as vuex
. Because main-project
has the augmented Vue
and dependency
has the original one, the two don't match.
TS Loader
During all my testing I couldn't reproduce the problem with just tsc
. Apparently ts-loader
does something differently causing this issue. It could have to do with it using Webpack for module resolution or it could be something entirely different. I don't know.
Solution Approaches
I have some ideas how this problem could be solved or worked around. Although these are not necessarily ready-to-use solutions but rather different approaches and ideas.
Add vuex
to dependency
As removing vuex
from main-project
isn't really an option, the only thing we can do to make both Vue
interfaces match, is include vuex
in dependency
as well.
The odd thing here is that I was able to get this fix working in my minimal example but not in the original project. I haven't figured out why that is.
In addition to that, it's not very elegant and you might have to import vuex
from every file that you reference from main-project
.
Use a shared node_modules
Having a shared node_modules
folder means both projects use the same vue
so this problem goes away. Depending on your requirements it might be a good solution to organize the two projects in a way that they share the same node_modules
folder. You might also want to take a look at tools like Lerna or Yarn workspaces which can help with this.
Consume dependency
as a package
You say that dependency
is not [an] npm-dependency yet. Maybe it's time to make it one. Similarly to a shared node_modules
directory this would result in both projects using the same vue
installation which should fix the issue.
Investigate further
As mentioned before, this only happens with ts-loader
. Maybe there is something that can be fixed or configured in ts-loader
to avoid this problem.
TL;DR
main-project
imports vuex
while dependency
doesn't. vuex
augments the Vue
interface using declaration merging adding a $store
property to it. Now the Vue
from one project doesn't match Vue
from other causing an error.
这篇关于Vue&TypeScript:在项目目录外的TypeScript组件中实现导入时如何避免错误TS2345?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!