From dfc7c0f12ad032753005602f4e35259cdb402e73 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 4 Nov 2019 18:38:55 -0500 Subject: [PATCH] refactor: adjust internal vnode types + more dts tests --- .github/contributing.md | 6 + .../__tests__/apiCreateComponent.spec.tsx | 174 -------------- .../__tests__/components/KeepAlive.spec.ts | 4 +- .../Suspense.spec.ts} | 0 packages/runtime-core/src/apiOptions.ts | 4 +- packages/runtime-core/src/apiWatch.ts | 5 +- packages/runtime-core/src/component.ts | 2 +- packages/runtime-core/src/componentProxy.ts | 1 - .../runtime-core/src/components/KeepAlive.ts | 15 +- .../Suspense.ts} | 40 +++- packages/runtime-core/src/h.ts | 14 +- packages/runtime-core/src/index.ts | 10 +- packages/runtime-core/src/renderer.ts | 8 +- packages/runtime-core/src/vnode.ts | 26 +- packages/runtime-core/src/warning.ts | 2 +- packages/runtime-dom/jsx.d.ts | 36 ++- rollup.config.js | 2 +- test-dts/createComponent.test-d.tsx | 224 ++++++++++++++++++ test-dts/h.test-d.ts | 172 ++++++++------ test-dts/index.d.ts | 6 + test-dts/tsx.test-d.tsx | 41 ++++ test-dts/util.ts | 4 + tsconfig.json | 5 +- 23 files changed, 486 insertions(+), 315 deletions(-) delete mode 100644 packages/runtime-core/__tests__/apiCreateComponent.spec.tsx rename packages/runtime-core/__tests__/{suspense.spec.ts => components/Suspense.spec.ts} (100%) rename packages/runtime-core/src/{rendererSuspense.ts => components/Suspense.ts} (90%) create mode 100644 test-dts/createComponent.test-d.tsx create mode 100644 test-dts/tsx.test-d.tsx create mode 100644 test-dts/util.ts diff --git a/.github/contributing.md b/.github/contributing.md index 2788016121..3d2733ad0f 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -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 index 3918d742cb..0000000000 --- a/packages/runtime-core/__tests__/apiCreateComponent.spec.tsx +++ /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, - // required + type casting - dd: { - type: Array as PropType, - 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 - ; -}) - -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) - } - }) - ; -}) - -test('type inference w/ direct setup function', () => { - const Comp = createComponent((props: { msg: string }) => { - return () =>
{props.msg}
- }) - ; -}) - -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 - } - }) - ; -}) - -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) - } - }) -}) diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts index ef15b7f4bd..9b9e3cc0d4 100644 --- a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts @@ -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(), diff --git a/packages/runtime-core/__tests__/suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts similarity index 100% rename from packages/runtime-core/__tests__/suspense.spec.ts rename to packages/runtime-core/__tests__/components/Suspense.spec.ts diff --git a/packages/runtime-core/src/apiOptions.ts b/packages/runtime-core/src/apiOptions.ts index fd4523e9b3..829dfdcc76 100644 --- a/packages/runtime-core/src/apiOptions.ts +++ b/packages/runtime-core/src/apiOptions.ts @@ -83,7 +83,7 @@ export type ComponentOptionsWithoutProps< M extends MethodOptions = {} > = ComponentOptionsBase & { props?: undefined -} & ThisType> +} & ThisType> 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)) { diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 2673b8dccc..e6401fcdc8 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -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) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 621f95bcd9..e1ea376591 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -25,7 +25,7 @@ import { makeMap, isPromise } from '@vue/shared' -import { SuspenseBoundary } from './rendererSuspense' +import { SuspenseBoundary } from './components/Suspense' import { CompilerError, CompilerOptions, diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 6cc1fcf87f..312577b5e8 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -26,7 +26,6 @@ export type ComponentPublicInstance< M extends MethodOptions = {}, PublicProps = P > = { - [key: string]: any $data: D $props: PublicProps $attrs: Data diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index d618c85506..0f37c99961 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -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 diff --git a/packages/runtime-core/src/rendererSuspense.ts b/packages/runtime-core/src/components/Suspense.ts similarity index 90% rename from packages/runtime-core/src/rendererSuspense.ts rename to packages/runtime-core/src/components/Suspense.ts index ab39a5ea76..3bf4f6cd53 100644 --- a/packages/runtime-core/src/rendererSuspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -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, diff --git a/packages/runtime-core/src/h.ts b/packages/runtime-core/src/h.ts index 73f74af998..7fe3c34745 100644 --- a/packages/runtime-core/src/h.ts +++ b/packages/runtime-core/src/h.ts @@ -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

{ + __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 diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index e09d17e7fb..7729342f2b 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -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' diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 4185b7d454..2cae06e92e 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -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, diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 1eecd0303f..71227f3d9e 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -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 = diff --git a/packages/runtime-core/src/warning.ts b/packages/runtime-core/src/warning.ts index 8e5a84d691..7f0ff080b5 100644 --- a/packages/runtime-core/src/warning.ts +++ b/packages/runtime-core/src/warning.ts @@ -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 diff --git a/packages/runtime-dom/jsx.d.ts b/packages/runtime-dom/jsx.d.ts index 79b7a4508e..05a8654676 100644 --- a/packages/runtime-dom/jsx.d.ts +++ b/packages/runtime-dom/jsx.d.ts @@ -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 = { [K in StringKeyOf]?: E[K] extends Function ? E[K] : (payload: E[K]) => void } -type ElementAttrs = T & EventHandlers +type ReservedProps = { + key?: string | number + ref?: string | Ref | ((ref: Element | ComponentPublicInstance | null) => void) +} + +type ElementAttrs = T & EventHandlers & ReservedProps type NativeElements = { [K in StringKeyOf]: 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 {} diff --git a/rollup.config.js b/rollup.config.js index e689068f28..47b017c697 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -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 index 0000000000..96a088b18d --- /dev/null +++ b/test-dts/createComponent.test-d.tsx @@ -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, + // required + type casting + dd: { + type: Array as PropType, + 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(props.a) + expectType(props.b) + expectType(props.bb) + expectType(props.cc) + expectType(props.dd) + expectType(props.ccc) + expectType(props.ddd) + + // setup context + return { + c: ref(1), + d: { + e: ref('hi') + } + } + }, + render() { + const props = this.$props + expectType(props.a) + expectType(props.b) + expectType(props.bb) + expectType(props.cc) + expectType(props.dd) + expectType(props.ccc) + expectType(props.ddd) + + // should also expose declared props on `this` + expectType(this.a) + expectType(this.b) + expectType(this.bb) + expectType(this.cc) + expectType(this.dd) + expectType(this.ccc) + expectType(this.ddd) + + // assert setup context unwrapping + expectType(this.c) + expectType(this.d.e) + + return null + } + }) + + // Test TSX + expectType( + + ) + + // missing required props + expectError() + + // wrong prop types + expectError( + + ) + expectError() +}) + +describe('type inference w/ optional props declaration', () => { + const MyComponent = createComponent({ + setup(_props: { msg: string }) { + return { + a: 1 + } + }, + render() { + expectType(this.$props.msg) + expectError(this.msg) + expectType(this.a) + return null + } + }) + + expectType() + expectError() + expectError() +}) + +describe('type inference w/ direct setup function', () => { + const MyComponent = createComponent((_props: { msg: string }) => {}) + expectType() + expectError() + expectError() +}) + +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(this.a) + expectType(this.b) + expectType(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(this.a) + return { + c: this.a || 123 + } + }, + computed: { + d(): number { + expectType(this.b) + return this.b + 1 + } + }, + watch: { + a() { + expectType(this.b) + this.b + 1 + } + }, + created() { + // props + expectType(this.a) + // returned from setup() + expectType(this.b) + // returned from data() + expectType(this.c) + // computed + expectType(this.d) + }, + methods: { + doSomething() { + // props + expectType(this.a) + // returned from setup() + expectType(this.b) + // returned from data() + expectType(this.c) + // computed + expectType(this.d) + } + }, + render() { + // props + expectType(this.a) + // returned from setup() + expectType(this.b) + // returned from data() + expectType(this.c) + // computed + expectType(this.d) + } + }) +}) diff --git a/test-dts/h.test-d.ts b/test-dts/h.test-d.ts index ae446fc2c5..a017dafc64 100644 --- a/test-dts/h.test-d.ts +++ b/test-dts/h.test-d.ts @@ -1,85 +1,117 @@ -// 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 })) +}) diff --git a/test-dts/index.d.ts b/test-dts/index.d.ts index 472a9bffd6..f61c077a50 100644 --- a/test-dts/index.d.ts +++ b/test-dts/index.d.ts @@ -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 index 0000000000..34075ed1b9 --- /dev/null +++ b/test-dts/tsx.test-d.tsx @@ -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(

) +expectType(
) +expectType() + +// unknown prop +expectError(
) + +// allow key/ref on arbitrary element +expectType(
) +expectType(
) + +expectType( + { + // infer correct event type + expectType(e.target) + }} + /> +) + +// built-in types +expectType() +expectType() + +expectType() +// target is required +expectError() + +// KeepAlive +expectType() +expectError() + +// Suspense +expectType() +expectType( {}} onRecede={() => {}} />) +expectError() diff --git a/test-dts/util.ts b/test-dts/util.ts new file mode 100644 index 0000000000..7745382476 --- /dev/null +++ b/test-dts/util.ts @@ -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) {} diff --git a/tsconfig.json b/tsconfig.json index 3784a8e29d..e14a5edd08 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" ] } -- 2.47.3