Repository: vuejs/vue
Pull request: fix(types): async Component types

<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->

<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [x] Bugfix
- [ ] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [ ] No
- [x] Not sure if this is considered a breaking change 

If yes, please describe the impact and migration path for existing applications:


**The PR fulfills these requirements:**

- [x] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch
- [x] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [x] New/updated tests are included

**Other information:**
Look at this codeexample:
```ts
import { AsyncComponent, Component } from 'vue'
const a: AsyncComponent = () => ({
  component: new Promise<Component>((res, rej) => {
    res({})
  }),
})

const b: AsyncComponent = () => ({
  component: () =>
    new Promise<Component>((res, rej) => {
      res({})
    }),
})
```

The first example is like in the docs https://vuejs.org/v2/guide/components-dynamic-async.html#Handling-Loading-State and the second example should not work (I also looked at the implementation and I think this would not work).

The types are currently like such that the first example throws a type Error and the second throws no error. This PR fixes that.


Changed files:
filename: types/options.d.ts
patch: @@ -12,9 +12,10 @@ export type Component<Data=DefaultData<never>, Methods=DefaultMethods<never>, Co
   | FunctionalComponentOptions<Props>
   | ComponentOptions<never, Data, Methods, Computed, Props>
 
-interface EsModuleComponent {
-  default: Component
-}
+type EsModule<T> = T | { default: T }
+
+type ImportedComponent<Data=DefaultData<never>, Methods=DefaultMethods<never>, Computed=DefaultComputed, Props=DefaultProps>
+  = EsModule<Component<Data, Methods, Computed, Props>>
 
 export type AsyncComponent<Data=DefaultData<never>, Methods=DefaultMethods<never>, Computed=DefaultComputed, Props=DefaultProps>
   = AsyncComponentPromise<Data, Methods, Computed, Props>
@@ -23,12 +24,12 @@ export type AsyncComponent<Data=DefaultData<never>, Methods=DefaultMethods<never
 export type AsyncComponentPromise<Data=DefaultData<never>, Methods=DefaultMethods<never>, Computed=DefaultComputed, Props=DefaultProps> = (
   resolve: (component: Component<Data, Methods, Computed, Props>) => void,
   reject: (reason?: any) => void
-) => Promise<Component | EsModuleComponent> | void;
+) => Promise<ImportedComponent<Data, Methods, Computed, Props>> | void;
 
 export type AsyncComponentFactory<Data=DefaultData<never>, Methods=DefaultMethods<never>, Computed=DefaultComputed, Props=DefaultProps> = () => {
-  component: AsyncComponentPromise<Data, Methods, Computed, Props>;
-  loading?: Component | EsModuleComponent;
-  error?: Component | EsModuleComponent;
+  component: Promise<ImportedComponent<Data, Methods, Computed, Props>>;
+  loading?: ImportedComponent;
+  error?: ImportedComponent;
   delay?: number;
   timeout?: number;
 }

filename: types/test/async-component-test.ts
patch: @@ -0,0 +1,44 @@
+import Vue, { AsyncComponent, Component } from "../index";
+
+const a: AsyncComponent = () => ({
+  component: new Promise<Component>((res, rej) => {
+    res({ template: "" })
+  })
+})
+
+const b: AsyncComponent = () => ({
+  // @ts-expect-error component has to be a Promise that resolves to a component
+  component: () =>
+    new Promise<Component>((res, rej) => {
+      res({ template: "" })
+    })
+})
+
+const c: AsyncComponent = () =>
+  new Promise<Component>((res, rej) => {
+    res({
+      template: ""
+    })
+  })
+
+const d: AsyncComponent = () =>
+  new Promise<{ default: Component }>((res, rej) => {
+    res({
+      default: {
+        template: ""
+      }
+    })
+  })
+
+const e: AsyncComponent = () => ({
+  component: new Promise<{ default: Component }>((res, rej) => {
+    res({
+      default: {
+        template: ""
+      }
+    })
+  })
+})
+
+// Test that Vue.component accepts any AsyncComponent
+Vue.component("async-compponent1", a)