Repository: vuejs/vue
Pull request: fix(types): broaden VNodeChildren, fix #8498

<!--
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
- [x] No

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:**
fixes #8498

Still not sure about `VNodeComponentOptions.children`, I don't know if that gets normalised or not. 

Changed files:
filename: types/test/options-test.ts
patch: @@ -281,6 +281,12 @@ Vue.component('provide-function', {
   })
 })
 
+Vue.component('component-with-slot', {
+  render (h): VNode {
+    return h('div', this.$slots.default)
+  }
+})
+
 Vue.component('component-with-scoped-slot', {
   render (h) {
     interface ScopedSlotProps {
@@ -301,6 +307,12 @@ Vue.component('component-with-scoped-slot', {
       h('child', {
         // Passing down all slots from parent
         scopedSlots: this.$scopedSlots
+      }),
+      h('child', {
+        // Passing down single slot from parent
+        scopedSlots: {
+          default: this.$scopedSlots.default
+        }
       })
     ])
   },
@@ -320,16 +332,22 @@ Vue.component('narrow-array-of-vnode-type', {
   render (h): VNode {
     const slot = this.$scopedSlots.default!({})
     if (typeof slot === 'string') {
+      // <template slot-scope="data">bare string</template>
       return h('span', slot)
     } else if (Array.isArray(slot)) {
+      // template with multiple children
       const first = slot[0]
-      if (!Array.isArray(first) && typeof first !== 'string') {
+      if (!Array.isArray(first) && typeof first !== 'string' && first) {
         return first
       } else {
         return h()
       }
-    } else {
+    } else if (slot) {
+      // <div slot-scope="data">bare VNode</div>
       return slot
+    } else {
+      // empty template, slot === undefined
+      return h()
     }
   }
 })

filename: types/vnode.d.ts
patch: @@ -1,9 +1,14 @@
 import { Vue } from "./vue";
 
-export type ScopedSlot = (props: any) => VNodeChildrenArrayContents | VNode | string;
+// Scoped slots can technically return anything if used from
+// a render function, but this is "good enough" for templates
+export type ScopedSlot = (props: any) => ScopedSlotChildren;
+export type ScopedSlotChildren = ScopedSlotArrayContents | VNode | string | undefined;
+export interface ScopedSlotArrayContents extends Array<ScopedSlotChildren> {}
 
-export type VNodeChildren = VNodeChildrenArrayContents | [ScopedSlot] | string;
-export interface VNodeChildrenArrayContents extends Array<VNode | string | VNodeChildrenArrayContents> {}
+// Relaxed type compatible with $createElement
+export type VNodeChildren = VNodeChildrenArrayContents | [ScopedSlot] | string | boolean | null | undefined;
+export interface VNodeChildrenArrayContents extends Array<VNodeChildren | VNode> {}
 
 export interface VNode {
   tag?: string;
@@ -27,7 +32,7 @@ export interface VNodeComponentOptions {
   Ctor: typeof Vue;
   propsData?: object;
   listeners?: object;
-  children?: VNodeChildren;
+  children?: VNode[];
   tag?: string;
 }