]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor: adjust internal vnode types + more dts tests
authorEvan You <yyx990803@gmail.com>
Mon, 4 Nov 2019 23:38:55 +0000 (18:38 -0500)
committerEvan You <yyx990803@gmail.com>
Mon, 4 Nov 2019 23:38:55 +0000 (18:38 -0500)
23 files changed:
.github/contributing.md
packages/runtime-core/__tests__/apiCreateComponent.spec.tsx [deleted file]
packages/runtime-core/__tests__/components/KeepAlive.spec.ts
packages/runtime-core/__tests__/components/Suspense.spec.ts [moved from packages/runtime-core/__tests__/suspense.spec.ts with 100% similarity]
packages/runtime-core/src/apiOptions.ts
packages/runtime-core/src/apiWatch.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/components/KeepAlive.ts
packages/runtime-core/src/components/Suspense.ts [moved from packages/runtime-core/src/rendererSuspense.ts with 90% similarity]
packages/runtime-core/src/h.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/vnode.ts
packages/runtime-core/src/warning.ts
packages/runtime-dom/jsx.d.ts
rollup.config.js
test-dts/createComponent.test-d.tsx [new file with mode: 0644]
test-dts/h.test-d.ts
test-dts/index.d.ts
test-dts/tsx.test-d.tsx [new file with mode: 0644]
test-dts/util.ts [new file with mode: 0644]
tsconfig.json

index 2788016121f4c981ea1c2e2ad6718e7732c0ef2f..3d2733ad0f17ec66eba61c73299d1ebf9f692acd 100644 (file)
@@ -174,6 +174,12 @@ Unit tests are collocated with the code being tested in each package, inside dir
 
 - Only use platform-specific runtimes if the test is asserting platform-specific behavior.
 
+### Testing Type Definition Correctness
+
+This project uses [tsd](https://github.com/SamVerschueren/tsd) to test the built definition files (`*.d.ts`).
+
+Type tests are located in the `test-dts` directory. To run the dts tests, run `yarn test-dts`. Note that the type test requires all relevant `*.d.ts` files to be built first (and the script does it for you). Once the `d.ts` files are built and up-to-date, the tests can be re-run by simply running `./node_modules/.bin/tsd`.
+
 ## Financial Contribution
 
 As a pure community-driven project without major corporate backing, we also welcome financial contributions via Patreon and OpenCollective.
diff --git a/packages/runtime-core/__tests__/apiCreateComponent.spec.tsx b/packages/runtime-core/__tests__/apiCreateComponent.spec.tsx
deleted file mode 100644 (file)
index 3918d74..0000000
+++ /dev/null
@@ -1,174 +0,0 @@
-import { createComponent } from '../src/apiCreateComponent'
-import { ref } from '@vue/reactivity'
-import { PropType } from '../src/componentProps'
-import { h } from '../src/h'
-
-// mock React just for TSX testing purposes
-const React = {
-  createElement: () => {}
-}
-
-test('createComponent type inference', () => {
-  const MyComponent = createComponent({
-    props: {
-      a: Number,
-      // required should make property non-void
-      b: {
-        type: String,
-        required: true
-      },
-      // default value should infer type and make it non-void
-      bb: {
-        default: 'hello'
-      },
-      // explicit type casting
-      cc: Array as PropType<string[]>,
-      // required + type casting
-      dd: {
-        type: Array as PropType<string[]>,
-        required: true
-      },
-      // explicit type casting with constructor
-      ccc: Array as () => string[],
-      // required + contructor type casting
-      ddd: {
-        type: Array as () => string[],
-        required: true
-      }
-    },
-    setup(props) {
-      props.a && props.a * 2
-      props.b.slice()
-      props.bb.slice()
-      props.cc && props.cc.push('hoo')
-      props.dd.push('dd')
-      return {
-        c: ref(1),
-        d: {
-          e: ref('hi')
-        }
-      }
-    },
-    render() {
-      const props = this.$props
-      props.a && props.a * 2
-      props.b.slice()
-      props.bb.slice()
-      props.cc && props.cc.push('hoo')
-      props.dd.push('dd')
-      this.a && this.a * 2
-      this.b.slice()
-      this.bb.slice()
-      this.c * 2
-      this.d.e.slice()
-      this.cc && this.cc.push('hoo')
-      this.dd.push('dd')
-      return h('div', this.bb)
-    }
-  })
-  // test TSX props inference
-  ;<MyComponent
-    a={1}
-    b="foo"
-    dd={['foo']}
-    ddd={['foo']}
-    // should allow extraneous as attrs
-    class="bar"
-  />
-})
-
-test('type inference w/ optional props declaration', () => {
-  const Comp = createComponent({
-    setup(props: { msg: string }) {
-      props.msg
-      return {
-        a: 1
-      }
-    },
-    render() {
-      this.$props.msg
-      this.msg
-      this.a * 2
-      return h('div', this.msg)
-    }
-  })
-  ;<Comp msg="hello" />
-})
-
-test('type inference w/ direct setup function', () => {
-  const Comp = createComponent((props: { msg: string }) => {
-    return () => <div>{props.msg}</div>
-  })
-  ;<Comp msg="hello" />
-})
-
-test('type inference w/ array props declaration', () => {
-  const Comp = createComponent({
-    props: ['a', 'b'],
-    setup(props) {
-      props.a
-      props.b
-      return {
-        c: 1
-      }
-    },
-    render() {
-      this.$props.a
-      this.$props.b
-      this.a
-      this.b
-      this.c
-    }
-  })
-  ;<Comp a={1} b={2} />
-})
-
-test('with legacy options', () => {
-  createComponent({
-    props: { a: Number },
-    setup() {
-      return {
-        b: 123
-      }
-    },
-    data() {
-      // Limitation: we cannot expose the return result of setup() on `this`
-      // here in data() - somehow that would mess up the inference
-      return {
-        c: this.a || 123
-      }
-    },
-    computed: {
-      d(): number {
-        return this.b + 1
-      }
-    },
-    watch: {
-      a() {
-        this.b + 1
-      }
-    },
-    created() {
-      this.a && this.a * 2
-      this.b * 2
-      this.c * 2
-      this.d * 2
-    },
-    methods: {
-      doSomething() {
-        this.a && this.a * 2
-        this.b * 2
-        this.c * 2
-        this.d * 2
-        return (this.a || 0) + this.b + this.c + this.d
-      }
-    },
-    render() {
-      this.a && this.a * 2
-      this.b * 2
-      this.c * 2
-      this.d * 2
-      return h('div', (this.a || 0) + this.b + this.c + this.d)
-    }
-  })
-})
index ef15b7f4bd86ec02039141ad2a3eabb2598c936f..9b9e3cc0d411041166122864aea3abb8739d106b 100644 (file)
@@ -22,7 +22,7 @@ describe('keep-alive', () => {
     one = {
       name: 'one',
       data: () => ({ msg: 'one' }),
-      render() {
+      render(this: any) {
         return h('div', this.msg)
       },
       created: jest.fn(),
@@ -34,7 +34,7 @@ describe('keep-alive', () => {
     two = {
       name: 'two',
       data: () => ({ msg: 'two' }),
-      render() {
+      render(this: any) {
         return h('div', this.msg)
       },
       created: jest.fn(),
index fd4523e9b39df2b80ce99a4f7eb248db0bcc2c8e..829dfdcc7625819530f24f2be741acf16d2f3478 100644 (file)
@@ -83,7 +83,7 @@ export type ComponentOptionsWithoutProps<
   M extends MethodOptions = {}
 > = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
   props?: undefined
-} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>>
+} & ThisType<ComponentPublicInstance<{}, RawBindings, D, C, M, Props>>
 
 export type ComponentOptionsWithArrayProps<
   PropNames extends string = string,
@@ -459,7 +459,7 @@ function createWatcher(
   ctx: ComponentPublicInstance,
   key: string
 ) {
-  const getter = () => ctx[key]
+  const getter = () => (ctx as Data)[key]
   if (isString(raw)) {
     const handler = renderContext[raw]
     if (isFunction(handler)) {
index 2673b8dccc7bbc2fafc63d2bfd0682d7f565c2a2..e6401fcdc8ab0d19bb1defa36de8f56e3b3d0642 100644 (file)
@@ -19,7 +19,8 @@ import { recordEffect } from './apiReactivity'
 import {
   currentInstance,
   ComponentInternalInstance,
-  currentSuspense
+  currentSuspense,
+  Data
 } from './component'
 import {
   ErrorCodes,
@@ -219,7 +220,7 @@ export function instanceWatch(
   cb: Function,
   options?: WatchOptions
 ): StopHandle {
-  const ctx = this.renderProxy!
+  const ctx = this.renderProxy as Data
   const getter = isString(source) ? () => ctx[source] : source.bind(ctx)
   const stop = watch(getter, cb.bind(ctx), options)
   onBeforeUnmount(stop, this)
index 621f95bcd9e587a816f7bc96bdd812b4867a2e4d..e1ea37659136dbf21fcdae3eacfaff3ab408928e 100644 (file)
@@ -25,7 +25,7 @@ import {
   makeMap,
   isPromise
 } from '@vue/shared'
-import { SuspenseBoundary } from './rendererSuspense'
+import { SuspenseBoundary } from './components/Suspense'
 import {
   CompilerError,
   CompilerOptions,
index 6cc1fcf87f8da13425f38f87279cb8a419e38dbe..312577b5e8eb0031b042f0cb3d563ca5c2a87741 100644 (file)
@@ -26,7 +26,6 @@ export type ComponentPublicInstance<
   M extends MethodOptions = {},
   PublicProps = P
 > = {
-  [key: string]: any
   $data: D
   $props: PublicProps
   $attrs: Data
index d618c85506cf7fa956b6fb135b685bf38a766a54..0f37c99961695c27e6d7ded6a13330701bd15407 100644 (file)
@@ -13,7 +13,7 @@ import { onBeforeUnmount, injectHook, onUnmounted } from '../apiLifecycle'
 import { isString, isArray } from '@vue/shared'
 import { watch } from '../apiWatch'
 import { ShapeFlags } from '../shapeFlags'
-import { SuspenseBoundary } from '../rendererSuspense'
+import { SuspenseBoundary } from './Suspense'
 import {
   RendererInternals,
   queuePostRenderEffect,
@@ -39,7 +39,7 @@ export interface KeepAliveSink {
   deactivate: (vnode: VNode) => void
 }
 
-export const KeepAlive = {
+const KeepAliveImpl = {
   name: `KeepAlive`,
 
   // Marker for special handling inside the renderer. We are not using a ===
@@ -201,13 +201,20 @@ export const KeepAlive = {
 }
 
 if (__DEV__) {
-  ;(KeepAlive as any).props = {
+  ;(KeepAliveImpl as any).props = {
     include: [String, RegExp, Array],
     exclude: [String, RegExp, Array],
     max: [String, Number]
   }
 }
 
+// export the public type for h/tsx inference
+export const KeepAlive = (KeepAliveImpl as any) as {
+  new (): {
+    $props: KeepAliveProps
+  }
+}
+
 function getName(comp: Component): string | void {
   return (comp as FunctionalComponent).displayName || comp.name
 }
@@ -268,7 +275,7 @@ function registerKeepAliveHook(
   if (target) {
     let current = target.parent
     while (current && current.parent) {
-      if (current.parent.type === KeepAlive) {
+      if (current.parent.type === KeepAliveImpl) {
         injectToKeepAliveRoot(wrappedHook, type, target, current)
       }
       current = current.parent
similarity index 90%
rename from packages/runtime-core/src/rendererSuspense.ts
rename to packages/runtime-core/src/components/Suspense.ts
index ab39a5ea76c04fa78890659fdc1864d3debb6cbe..3bf4f6cd53d96e7a7e00023edc90fea36f117a62 100644 (file)
@@ -1,15 +1,27 @@
-import { VNode, normalizeVNode, VNodeChild } from './vnode'
-import { ShapeFlags } from './shapeFlags'
+import { VNode, normalizeVNode, VNodeChild } from '../vnode'
+import { ShapeFlags } from '../shapeFlags'
 import { isFunction, isArray } from '@vue/shared'
-import { ComponentInternalInstance, handleSetupResult } from './component'
-import { Slots } from './componentSlots'
-import { RendererInternals } from './renderer'
-import { queuePostFlushCb, queueJob } from './scheduler'
-import { updateHOCHostEl } from './componentRenderUtils'
-import { handleError, ErrorCodes } from './errorHandling'
-import { pushWarningContext, popWarningContext } from './warning'
+import { ComponentInternalInstance, handleSetupResult } from '../component'
+import { Slots } from '../componentSlots'
+import { RendererInternals } from '../renderer'
+import { queuePostFlushCb, queueJob } from '../scheduler'
+import { updateHOCHostEl } from '../componentRenderUtils'
+import { handleError, ErrorCodes } from '../errorHandling'
+import { pushWarningContext, popWarningContext } from '../warning'
 
-export const Suspense = {
+export interface SuspenseProps {
+  onResolve?: () => void
+  onRecede?: () => void
+}
+
+// Suspense exposes a component-like API, and is treated like a component
+// in the compiler, but internally it's a special built-in type that hooks
+// directly into the renderer.
+export const SuspenseImpl = {
+  // In order to make Suspense tree-shakable, we need to avoid importing it
+  // directly in the renderer. The renderer checks for the __isSuspense flag
+  // on a vnode's type and calls the `process` method, passing in renderer
+  // internals.
   __isSuspense: true,
   process(
     n1: VNode | null,
@@ -49,6 +61,14 @@ export const Suspense = {
   }
 }
 
+// Force-casted public typing for h and TSX props inference
+export const Suspense = ((__FEATURE_SUSPENSE__
+  ? SuspenseImpl
+  : null) as any) as {
+  __isSuspense: true
+  new (): { $props: SuspenseProps }
+}
+
 function mountSuspense(
   n2: VNode,
   container: object,
index 73f74af998c5cfa7834cef810454d4a93683f482..7fe3c347457b9aa2e90dae4e9238366ab548aebf 100644 (file)
@@ -5,9 +5,9 @@ import {
   VNodeChildren,
   Fragment,
   Portal,
-  isVNode,
-  Suspense
+  isVNode
 } from './vnode'
+import { Suspense, SuspenseProps } from './components/Suspense'
 import { isObject, isArray } from '@vue/shared'
 import { RawSlots } from './componentSlots'
 import { FunctionalComponent } from './component'
@@ -67,6 +67,9 @@ type RawChildren =
 
 // fake constructor type returned from `createComponent`
 interface Constructor<P = any> {
+  __isFragment?: never
+  __isPortal?: never
+  __isSuspense?: never
   new (): { $props: P }
 }
 
@@ -100,12 +103,7 @@ export function h(
 export function h(type: typeof Suspense, children?: RawChildren): VNode
 export function h(
   type: typeof Suspense,
-  props?:
-    | (RawProps & {
-        onResolve?: () => void
-        onRecede?: () => void
-      })
-    | null,
+  props?: (RawProps & SuspenseProps) | null,
   children?: RawChildren | RawSlots
 ): VNode
 
index e09d17e7fbf54b2ddb626e8323b61424bb1d869e..7729342f2bf5079a3784a6d266f54e365d475e05 100644 (file)
@@ -1,5 +1,6 @@
 // Public API ------------------------------------------------------------------
 
+export const version = __VERSION__
 export * from './apiReactivity'
 export * from './apiWatch'
 export * from './apiLifecycle'
@@ -23,9 +24,10 @@ export {
   createBlock
 } from './vnode'
 // VNode type symbols
-export { Text, Comment, Fragment, Portal, Suspense } from './vnode'
+export { Text, Comment, Fragment, Portal } from './vnode'
 // Internal Components
-export { KeepAlive } from './components/KeepAlive'
+export { Suspense, SuspenseProps } from './components/Suspense'
+export { KeepAlive, KeepAliveProps } from './components/KeepAlive'
 // VNode flags
 export { PublicShapeFlags as ShapeFlags } from './shapeFlags'
 import { PublicPatchFlags } from '@vue/shared'
@@ -111,6 +113,4 @@ export {
   FunctionDirective,
   DirectiveArguments
 } from './directives'
-export { SuspenseBoundary } from './rendererSuspense'
-
-export const version = __VERSION__
+export { SuspenseBoundary } from './components/Suspense'
index 4185b7d454a6bfc90f1768cbffc667e6b3a7b6ac..2cae06e92ee8937276dd0afe36981d8bed340a63 100644 (file)
@@ -47,9 +47,9 @@ import { ComponentPublicInstance } from './componentProxy'
 import { App, createAppAPI } from './apiApp'
 import {
   SuspenseBoundary,
-  Suspense,
-  queueEffectWithSuspense
-} from './rendererSuspense'
+  queueEffectWithSuspense,
+  SuspenseImpl
+} from './components/Suspense'
 import { ErrorCodes, callWithErrorHandling } from './errorHandling'
 import { KeepAliveSink } from './components/KeepAlive'
 
@@ -265,7 +265,7 @@ export function createRenderer<
             optimized
           )
         } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
-          ;(type as typeof Suspense).process(
+          ;(type as typeof SuspenseImpl).process(
             n1,
             n2,
             container,
index 1eecd0303fcdd4476980ffa939f046cd6098e20b..71227f3d9e94e40e82d54c2663ccbb6950f9f4ab 100644 (file)
@@ -16,31 +16,25 @@ import { RawSlots } from './componentSlots'
 import { ShapeFlags } from './shapeFlags'
 import { isReactive, Ref } from '@vue/reactivity'
 import { AppContext } from './apiApp'
-import { SuspenseBoundary } from './rendererSuspense'
+import { SuspenseBoundary } from './components/Suspense'
 import { DirectiveBinding } from './directives'
-import { Suspense as SuspenseImpl } from './rendererSuspense'
+import { SuspenseImpl } from './components/Suspense'
 
 export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
-  // type differentiator for h()
   __isFragment: true
+  new (): {
+    $props: VNodeProps
+  }
 }
 export const Portal = (Symbol(__DEV__ ? 'Portal' : undefined) as any) as {
-  // type differentiator for h()
   __isPortal: true
+  new (): {
+    $props: VNodeProps & { target: string | object }
+  }
 }
 export const Text = Symbol(__DEV__ ? 'Text' : undefined)
 export const Comment = Symbol(__DEV__ ? 'Comment' : undefined)
 
-// Export Suspense with casting to avoid circular type dependency between
-// `suspense.ts` and `createRenderer.ts` in exported types.
-// A circular type dependency causes tsc to generate d.ts with dynmaic import()
-// calls using realtive paths, which works for separate d.ts files, but will
-// fail after d.ts rollup with API Extractor.
-const Suspense = ((__FEATURE_SUSPENSE__ ? SuspenseImpl : null) as any) as {
-  __isSuspense: true
-}
-export { Suspense }
-
 export type VNodeTypes =
   | string
   | Component
@@ -48,12 +42,12 @@ export type VNodeTypes =
   | typeof Portal
   | typeof Text
   | typeof Comment
-  | typeof Suspense
+  | typeof SuspenseImpl
 
 export interface VNodeProps {
   [key: string]: any
   key?: string | number
-  ref?: string | Ref | ((ref: object) => void)
+  ref?: string | Ref | ((ref: object | null) => void)
 }
 
 type VNodeChildAtom<HostNode, HostElement> =
index 8e5a84d69121c90e63c27619111c08292d15e82e..7f0ff080b59a2cd3a403cf063d35c3a7ae865fd8 100644 (file)
@@ -116,7 +116,7 @@ const classify = (str: string): string =>
   str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')
 
 function formatComponentName(vnode: ComponentVNode, file?: string): string {
-  const Component = vnode.type
+  const Component = vnode.type as Component
   let name = isFunction(Component)
     ? Component.displayName || Component.name
     : Component.name
index 79b7a4508ef0344870120d5ada2ac605240694ce..05a8654676e976610e3cf60353fa65fb44fe1180 100644 (file)
@@ -1,3 +1,5 @@
+import { Ref, ComponentPublicInstance } from '@vue/runtime-core'
+
 // This code is based on https://github.com/wonderful-panda/vue-tsx-support
 // published under the MIT license.
 // Copyright by @wonderful-panda
@@ -740,7 +742,12 @@ type EventHandlers<E> = {
   [K in StringKeyOf<E>]?: E[K] extends Function ? E[K] : (payload: E[K]) => void
 }
 
-type ElementAttrs<T> = T & EventHandlers<Events>
+type ReservedProps = {
+  key?: string | number
+  ref?: string | Ref | ((ref: Element | ComponentPublicInstance | null) => void)
+}
+
+type ElementAttrs<T> = T & EventHandlers<Events> & ReservedProps
 
 type NativeElements = {
   [K in StringKeyOf<IntrinsicElementAttributes>]: ElementAttrs<
@@ -748,16 +755,21 @@ type NativeElements = {
   >
 }
 
-declare namespace JSX {
-  interface Element {}
-  interface ElementClass {
-    $props: {}
-  }
-  interface ElementAttributesProperty {
-    $props: {}
-  }
-  interface IntrinsicElements extends NativeElements {
-    // allow arbitrary elements
-    [name: string]: any
+declare global {
+  namespace JSX {
+    interface Element {}
+    interface ElementClass {
+      $props: {}
+    }
+    interface ElementAttributesProperty {
+      $props: {}
+    }
+    interface IntrinsicElements extends NativeElements {
+      // allow arbitrary elements
+      [name: string]: any
+    }
   }
 }
+
+// suppress ts:2669
+export {}
index e689068f281be18e280df66155185771f2db0e3f..47b017c6973ac4f07ae3ef54aa8225f100da3408 100644 (file)
@@ -97,7 +97,7 @@ function createConfig(output, plugins = []) {
         declaration: shouldEmitDeclarations,
         declarationMap: shouldEmitDeclarations
       },
-      exclude: ['**/__tests__']
+      exclude: ['**/__tests__', 'test-dts']
     }
   })
   // we only need to check TS and generate declarations once for each build.
diff --git a/test-dts/createComponent.test-d.tsx b/test-dts/createComponent.test-d.tsx
new file mode 100644 (file)
index 0000000..96a088b
--- /dev/null
@@ -0,0 +1,224 @@
+import { describe } from './util'
+import { expectError, expectType } from 'tsd'
+import { createComponent, PropType, ref } from './index'
+
+describe('with object props', () => {
+  interface ExpectedProps {
+    a?: number | undefined
+    b: string
+    bb: string
+    cc?: string[] | undefined
+    dd: string[]
+    ccc?: string[] | undefined
+    ddd: string[]
+  }
+
+  const MyComponent = createComponent({
+    props: {
+      a: Number,
+      // required should make property non-void
+      b: {
+        type: String,
+        required: true
+      },
+      // default value should infer type and make it non-void
+      bb: {
+        default: 'hello'
+      },
+      // explicit type casting
+      cc: Array as PropType<string[]>,
+      // required + type casting
+      dd: {
+        type: Array as PropType<string[]>,
+        required: true
+      },
+      // explicit type casting with constructor
+      ccc: Array as () => string[],
+      // required + contructor type casting
+      ddd: {
+        type: Array as () => string[],
+        required: true
+      }
+    },
+    setup(props) {
+      // type assertion. See https://github.com/SamVerschueren/tsd
+      expectType<ExpectedProps['a']>(props.a)
+      expectType<ExpectedProps['b']>(props.b)
+      expectType<ExpectedProps['bb']>(props.bb)
+      expectType<ExpectedProps['cc']>(props.cc)
+      expectType<ExpectedProps['dd']>(props.dd)
+      expectType<ExpectedProps['ccc']>(props.ccc)
+      expectType<ExpectedProps['ddd']>(props.ddd)
+
+      // setup context
+      return {
+        c: ref(1),
+        d: {
+          e: ref('hi')
+        }
+      }
+    },
+    render() {
+      const props = this.$props
+      expectType<ExpectedProps['a']>(props.a)
+      expectType<ExpectedProps['b']>(props.b)
+      expectType<ExpectedProps['bb']>(props.bb)
+      expectType<ExpectedProps['cc']>(props.cc)
+      expectType<ExpectedProps['dd']>(props.dd)
+      expectType<ExpectedProps['ccc']>(props.ccc)
+      expectType<ExpectedProps['ddd']>(props.ddd)
+
+      // should also expose declared props on `this`
+      expectType<ExpectedProps['a']>(this.a)
+      expectType<ExpectedProps['b']>(this.b)
+      expectType<ExpectedProps['bb']>(this.bb)
+      expectType<ExpectedProps['cc']>(this.cc)
+      expectType<ExpectedProps['dd']>(this.dd)
+      expectType<ExpectedProps['ccc']>(this.ccc)
+      expectType<ExpectedProps['ddd']>(this.ddd)
+
+      // assert setup context unwrapping
+      expectType<number>(this.c)
+      expectType<string>(this.d.e)
+
+      return null
+    }
+  })
+
+  // Test TSX
+  expectType<JSX.Element>(
+    <MyComponent
+      a={1}
+      b="b"
+      bb="bb"
+      cc={['cc']}
+      dd={['dd']}
+      ccc={['ccc']}
+      ddd={['ddd']}
+      // should allow extraneous as attrs
+      class="bar"
+      // should allow key
+      key={'foo'}
+      // should allow ref
+      ref={'foo'}
+    />
+  )
+
+  // missing required props
+  expectError(<MyComponent />)
+
+  // wrong prop types
+  expectError(
+    <MyComponent a={'wrong type'} b="foo" dd={['foo']} ddd={['foo']} />
+  )
+  expectError(<MyComponent b="foo" dd={[123]} ddd={['foo']} />)
+})
+
+describe('type inference w/ optional props declaration', () => {
+  const MyComponent = createComponent({
+    setup(_props: { msg: string }) {
+      return {
+        a: 1
+      }
+    },
+    render() {
+      expectType<string>(this.$props.msg)
+      expectError(this.msg)
+      expectType<number>(this.a)
+      return null
+    }
+  })
+
+  expectType<JSX.Element>(<MyComponent msg="foo" />)
+  expectError(<MyComponent />)
+  expectError(<MyComponent msg={1} />)
+})
+
+describe('type inference w/ direct setup function', () => {
+  const MyComponent = createComponent((_props: { msg: string }) => {})
+  expectType<JSX.Element>(<MyComponent msg="foo" />)
+  expectError(<MyComponent />)
+  expectError(<MyComponent msg={1} />)
+})
+
+describe('type inference w/ array props declaration', () => {
+  createComponent({
+    props: ['a', 'b'],
+    setup(props) {
+      props.a
+      props.b
+      return {
+        c: 1
+      }
+    },
+    render() {
+      expectType<{ a?: any; b?: any }>(this.$props)
+      expectType<any>(this.a)
+      expectType<any>(this.b)
+      expectType<number>(this.c)
+    }
+  })
+})
+
+describe('type inference w/ options API', () => {
+  createComponent({
+    props: { a: Number },
+    setup() {
+      return {
+        b: 123
+      }
+    },
+    data() {
+      // Limitation: we cannot expose the return result of setup() on `this`
+      // here in data() - somehow that would mess up the inference
+      expectType<number | undefined>(this.a)
+      return {
+        c: this.a || 123
+      }
+    },
+    computed: {
+      d(): number {
+        expectType<number>(this.b)
+        return this.b + 1
+      }
+    },
+    watch: {
+      a() {
+        expectType<number>(this.b)
+        this.b + 1
+      }
+    },
+    created() {
+      // props
+      expectType<number | undefined>(this.a)
+      // returned from setup()
+      expectType<number>(this.b)
+      // returned from data()
+      expectType<number>(this.c)
+      // computed
+      expectType<number>(this.d)
+    },
+    methods: {
+      doSomething() {
+        // props
+        expectType<number | undefined>(this.a)
+        // returned from setup()
+        expectType<number>(this.b)
+        // returned from data()
+        expectType<number>(this.c)
+        // computed
+        expectType<number>(this.d)
+      }
+    },
+    render() {
+      // props
+      expectType<number | undefined>(this.a)
+      // returned from setup()
+      expectType<number>(this.b)
+      // returned from data()
+      expectType<number>(this.c)
+      // computed
+      expectType<number>(this.d)
+    }
+  })
+})
index ae446fc2c59825748c66a160cfe58c896dfc4592..a017dafc64ea74e834224f572d1a1798da923c67 100644 (file)
-// This file tests a number of cases that *should* fail using tsd:
-// https://github.com/SamVerschueren/tsd
-// It will probably show up red in VSCode, and it's intended. We cannot use
-// directives like @ts-ignore or @ts-nocheck since that would suppress the
-// errors that should be caught.
-
+import { describe } from './util'
 import { expectError } from 'tsd'
 import { h, createComponent, ref, Fragment, Portal, Suspense } from './index'
 
-// h inference w/ element
-// key
-h('div', { key: 1 })
-h('div', { key: 'foo' })
-expectError(h('div', { key: [] }))
-expectError(h('div', { key: {} }))
-// ref
-h('div', { ref: 'foo' })
-h('div', { ref: ref(null) })
-h('div', { ref: el => {} })
-expectError(h('div', { ref: [] }))
-expectError(h('div', { ref: {} }))
-expectError(h('div', { ref: 123 }))
+describe('h inference w/ element', () => {
+  // key
+  h('div', { key: 1 })
+  h('div', { key: 'foo' })
+  expectError(h('div', { key: [] }))
+  expectError(h('div', { key: {} }))
+  // ref
+  h('div', { ref: 'foo' })
+  h('div', { ref: ref(null) })
+  h('div', { ref: el => {} })
+  expectError(h('div', { ref: [] }))
+  expectError(h('div', { ref: {} }))
+  expectError(h('div', { ref: 123 }))
+})
 
-// h inference w/ Fragment
-// only accepts array children
-h(Fragment, ['hello'])
-h(Fragment, { key: 123 }, ['hello'])
-expectError(h(Fragment, 'foo'))
-expectError(h(Fragment, { key: 123 }, 'bar'))
+describe('h inference w/ Fragment', () => {
+  // only accepts array children
+  h(Fragment, ['hello'])
+  h(Fragment, { key: 123 }, ['hello'])
+  expectError(h(Fragment, 'foo'))
+  expectError(h(Fragment, { key: 123 }, 'bar'))
+})
 
-// h inference w/ Portal
-h(Portal, { target: '#foo' }, 'hello')
-expectError(h(Portal))
-expectError(h(Portal, {}))
-expectError(h(Portal, { target: '#foo' }))
+describe('h inference w/ Portal', () => {
+  h(Portal, { target: '#foo' }, 'hello')
+  expectError(h(Portal))
+  expectError(h(Portal, {}))
+  expectError(h(Portal, { target: '#foo' }))
+})
 
-// h inference w/ Suspense
-h(Suspense, { onRecede: () => {}, onResolve: () => {} }, 'hello')
-h(Suspense, 'foo')
-h(Suspense, () => 'foo')
-h(Suspense, null, {
-  default: () => 'foo'
+describe('h inference w/ Suspense', () => {
+  h(Suspense, { onRecede: () => {}, onResolve: () => {} }, 'hello')
+  h(Suspense, 'foo')
+  h(Suspense, () => 'foo')
+  h(Suspense, null, {
+    default: () => 'foo'
+  })
+  expectError(h(Suspense, { onResolve: 1 }))
 })
-expectError(h(Suspense, { onResolve: 1 }))
 
-// h inference w/ functional component
-const Func = (_props: { foo: string; bar?: number }) => ''
-h(Func, { foo: 'hello' })
-h(Func, { foo: 'hello', bar: 123 })
-expectError(h(Func, { foo: 123 }))
-expectError(h(Func, {}))
-expectError(h(Func, { bar: 123 }))
+describe('h inference w/ functional component', () => {
+  const Func = (_props: { foo: string; bar?: number }) => ''
+  h(Func, { foo: 'hello' })
+  h(Func, { foo: 'hello', bar: 123 })
+  expectError(h(Func, { foo: 123 }))
+  expectError(h(Func, {}))
+  expectError(h(Func, { bar: 123 }))
+})
 
-// h inference w/ plain object component
-const Foo = {
-  props: {
-    foo: String
+describe('h inference w/ plain object component', () => {
+  const Foo = {
+    props: {
+      foo: String
+    }
   }
-}
 
-h(Foo, { foo: 'ok' })
-h(Foo, { foo: 'ok', class: 'extra' })
-// should fail on wrong type
-expectError(h(Foo, { foo: 1 }))
+  h(Foo, { foo: 'ok' })
+  h(Foo, { foo: 'ok', class: 'extra' })
+  // should fail on wrong type
+  expectError(h(Foo, { foo: 1 }))
+})
 
-// h inference w/ createComponent
-const Bar = createComponent({
-  props: {
-    foo: String,
-    bar: {
-      type: Number,
-      required: true
+describe('h inference w/ createComponent', () => {
+  const Foo = createComponent({
+    props: {
+      foo: String,
+      bar: {
+        type: Number,
+        required: true
+      }
     }
-  }
+  })
+
+  h(Foo, { bar: 1 })
+  h(Foo, { bar: 1, foo: 'ok' })
+  // should allow extraneous props (attrs fallthrough)
+  h(Foo, { bar: 1, foo: 'ok', class: 'extra' })
+  // should fail on missing required prop
+  expectError(h(Foo, {}))
+  expectError(h(Foo, { foo: 'ok' }))
+  // should fail on wrong type
+  expectError(h(Foo, { bar: 1, foo: 1 }))
 })
 
-h(Bar, { bar: 1 })
-h(Bar, { bar: 1, foo: 'ok' })
-// should allow extraneous props (attrs fallthrough)
-h(Bar, { bar: 1, foo: 'ok', class: 'extra' })
-// should fail on missing required prop
-expectError(h(Bar, {}))
-expectError(h(Bar, { foo: 'ok' }))
-// should fail on wrong type
-expectError(h(Bar, { bar: 1, foo: 1 }))
+describe('h inference w/ createComponent + optional props', () => {
+  const Foo = createComponent({
+    setup(_props: { foo?: string; bar: number }) {}
+  })
+
+  h(Foo, { bar: 1 })
+  h(Foo, { bar: 1, foo: 'ok' })
+  // should allow extraneous props (attrs fallthrough)
+  h(Foo, { bar: 1, foo: 'ok', class: 'extra' })
+  // should fail on missing required prop
+  expectError(h(Foo, {}))
+  expectError(h(Foo, { foo: 'ok' }))
+  // should fail on wrong type
+  expectError(h(Foo, { bar: 1, foo: 1 }))
+})
+
+describe('h inference w/ createComponent + direct function', () => {
+  const Foo = createComponent((_props: { foo?: string; bar: number }) => {})
+
+  h(Foo, { bar: 1 })
+  h(Foo, { bar: 1, foo: 'ok' })
+  // should allow extraneous props (attrs fallthrough)
+  h(Foo, { bar: 1, foo: 'ok', class: 'extra' })
+  // should fail on missing required prop
+  expectError(h(Foo, {}))
+  expectError(h(Foo, { foo: 'ok' }))
+  // should fail on wrong type
+  expectError(h(Foo, { bar: 1, foo: 1 }))
+})
index 472a9bffd6e5f1724dadcd2e0b8f6ccc24455c6c..f61c077a50bb0205447f925fbb0f8eb18df9b130 100644 (file)
@@ -1 +1,7 @@
+// This directory contains a number of d.ts assertions using tsd:
+// https://github.com/SamVerschueren/tsd
+// The tests checks type errors and will probably show up red in VSCode, and
+// it's intended. We cannot use directives like @ts-ignore or @ts-nocheck since
+// that would suppress the errors that should be caught.
+
 export * from '@vue/runtime-dom'
diff --git a/test-dts/tsx.test-d.tsx b/test-dts/tsx.test-d.tsx
new file mode 100644 (file)
index 0000000..34075ed
--- /dev/null
@@ -0,0 +1,41 @@
+// TSX w/ createComponent is tested in createComponent.test-d.tsx
+
+import { expectError, expectType } from 'tsd'
+import { KeepAlive, Suspense, Fragment, Portal } from '@vue/runtime-dom'
+
+expectType<JSX.Element>(<div />)
+expectType<JSX.Element>(<div id="foo" />)
+expectType<JSX.Element>(<input value="foo" />)
+
+// unknown prop
+expectError(<div foo="bar" />)
+
+// allow key/ref on arbitrary element
+expectType<JSX.Element>(<div key="foo" />)
+expectType<JSX.Element>(<div ref="bar" />)
+
+expectType<JSX.Element>(
+  <input
+    onInput={e => {
+      // infer correct event type
+      expectType<EventTarget | null>(e.target)
+    }}
+  />
+)
+
+// built-in types
+expectType<JSX.Element>(<Fragment />)
+expectType<JSX.Element>(<Fragment key="1" />)
+
+expectType<JSX.Element>(<Portal target="#foo" />)
+// target is required
+expectError(<Portal />)
+
+// KeepAlive
+expectType<JSX.Element>(<KeepAlive include="foo" exclude={['a']} />)
+expectError(<KeepAlive include={123} />)
+
+// Suspense
+expectType<JSX.Element>(<Suspense />)
+expectType<JSX.Element>(<Suspense onResolve={() => {}} onRecede={() => {}} />)
+expectError(<Suspense onResolve={123} />)
diff --git a/test-dts/util.ts b/test-dts/util.ts
new file mode 100644 (file)
index 0000000..7745382
--- /dev/null
@@ -0,0 +1,4 @@
+// aesthetic utility for making test-d.ts look more like actual tests
+// and makes it easier to navigate test cases with folding
+// it's a noop since test-d.ts files are not actually run.
+export function describe(_name: string, _fn: () => void) {}
index 3784a8e29d1809a48e14f3893e431d6f80570f19..e14a5edd08f05d69cd65696812074a628b18c0fc 100644 (file)
@@ -15,7 +15,7 @@
     "resolveJsonModule": true,
     "esModuleInterop": true,
     "removeComments": false,
-    "jsx": "react",
+    "jsx": "preserve",
     "lib": ["esnext", "dom"],
     "types": ["jest", "node"],
     "rootDir": ".",
@@ -35,6 +35,7 @@
     "packages/global.d.ts",
     "packages/runtime-dom/jsx.d.ts",
     "packages/*/src",
-    "packages/*/__tests__"
+    "packages/*/__tests__",
+    "test-dts"
   ]
 }