feat: fix all cases for h and options type inference

This commit is contained in:
Evan You 2019-09-05 18:48:49 -04:00
parent 6c7cbb0dc9
commit 94a05561f8
9 changed files with 83 additions and 85 deletions

View File

@ -23,8 +23,8 @@ describe('api: createApp', () => {
default: 0 default: 0
} }
}, },
render() { setup(props: { count: number }) {
return this.count return () => props.count
} }
} }

View File

@ -1,4 +1,4 @@
import { createComponent, ComponentRenderProxy } from '../src/component' import { createComponent } from '../src/component'
import { ref } from '@vue/reactivity' import { ref } from '@vue/reactivity'
import { PropType } from '../src/componentProps' import { PropType } from '../src/componentProps'
import { h } from '../src/h' import { h } from '../src/h'
@ -56,7 +56,7 @@ test('createComponent type inference', () => {
this.d.e.slice() this.d.e.slice()
this.cc && this.cc.push('hoo') this.cc && this.cc.push('hoo')
this.dd.push('dd') this.dd.push('dd')
// return h('div', this.bb) return h('div', this.bb)
} }
}) })
// test TSX props inference // test TSX props inference
@ -75,7 +75,7 @@ test('type inference w/ optional props declaration', () => {
this.$props.msg this.$props.msg
this.msg this.msg
this.a * 2 this.a * 2
// return h('div', this.msg) return h('div', this.msg)
} }
}) })
;(<Comp msg="hello"/>) ;(<Comp msg="hello"/>)
@ -118,10 +118,10 @@ test('with legacy options', () => {
} }
}, },
data() { data() {
this.a // Limitation: we cannot expose the return result of setup() on `this`
this.b // here in data() - somehow that would mess up the inference
return { return {
c: 234 c: this.a || 123
} }
}, },
computed: { computed: {
@ -148,6 +148,13 @@ test('with legacy options', () => {
this.d * 2 this.d * 2
return (this.a || 0) + this.b + this.c + this.d 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)
} }
}) })
}) })

View File

@ -7,12 +7,13 @@ import {
TestElement, TestElement,
nextTick, nextTick,
renderToString, renderToString,
ref ref,
createComponent
} from '@vue/runtime-test' } from '@vue/runtime-test'
describe('api: options', () => { describe('api: options', () => {
test('data', async () => { test('data', async () => {
const Comp = { const Comp = createComponent({
data() { data() {
return { return {
foo: 1 foo: 1
@ -29,7 +30,7 @@ describe('api: options', () => {
this.foo this.foo
) )
} }
} })
const root = nodeOps.createElement('div') const root = nodeOps.createElement('div')
render(h(Comp), root) render(h(Comp), root)
expect(serializeInner(root)).toBe(`<div>1</div>`) expect(serializeInner(root)).toBe(`<div>1</div>`)
@ -40,17 +41,17 @@ describe('api: options', () => {
}) })
test('computed', async () => { test('computed', async () => {
const Comp = { const Comp = createComponent({
data() { data() {
return { return {
foo: 1 foo: 1
} }
}, },
computed: { computed: {
bar() { bar(): number {
return this.foo + 1 return this.foo + 1
}, },
baz() { baz(): number {
return this.bar + 1 return this.bar + 1
} }
}, },
@ -65,7 +66,7 @@ describe('api: options', () => {
this.bar + this.baz this.bar + this.baz
) )
} }
} })
const root = nodeOps.createElement('div') const root = nodeOps.createElement('div')
render(h(Comp), root) render(h(Comp), root)
expect(serializeInner(root)).toBe(`<div>5</div>`) expect(serializeInner(root)).toBe(`<div>5</div>`)
@ -76,7 +77,7 @@ describe('api: options', () => {
}) })
test('methods', async () => { test('methods', async () => {
const Comp = { const Comp = createComponent({
data() { data() {
return { return {
foo: 1 foo: 1
@ -96,7 +97,7 @@ describe('api: options', () => {
this.foo this.foo
) )
} }
} })
const root = nodeOps.createElement('div') const root = nodeOps.createElement('div')
render(h(Comp), root) render(h(Comp), root)
expect(serializeInner(root)).toBe(`<div>1</div>`) expect(serializeInner(root)).toBe(`<div>1</div>`)
@ -107,7 +108,7 @@ describe('api: options', () => {
}) })
test('watch', async () => { test('watch', async () => {
function returnThis() { function returnThis(this: any) {
return this return this
} }
const spyA = jest.fn(returnThis) const spyA = jest.fn(returnThis)
@ -188,20 +189,20 @@ describe('api: options', () => {
render() { render() {
return [h(ChildA), h(ChildB), h(ChildC), h(ChildD)] return [h(ChildA), h(ChildB), h(ChildC), h(ChildD)]
} }
} } as any
const ChildA = { const ChildA = {
inject: ['a'], inject: ['a'],
render() { render() {
return this.a return this.a
} }
} } as any
const ChildB = { const ChildB = {
// object alias // object alias
inject: { b: 'a' }, inject: { b: 'a' },
render() { render() {
return this.b return this.b
} }
} } as any
const ChildC = { const ChildC = {
inject: { inject: {
b: { b: {
@ -211,7 +212,7 @@ describe('api: options', () => {
render() { render() {
return this.b return this.b
} }
} } as any
const ChildD = { const ChildD = {
inject: { inject: {
b: { b: {
@ -222,7 +223,7 @@ describe('api: options', () => {
render() { render() {
return this.b return this.b
} }
} } as any
expect(renderToString(h(Root))).toBe(`<!---->1112<!---->`) expect(renderToString(h(Root))).toBe(`<!---->1112<!---->`)
}) })
@ -287,7 +288,7 @@ describe('api: options', () => {
unmounted() { unmounted() {
calls.push('mid onUnmounted') calls.push('mid onUnmounted')
}, },
render() { render(this: any) {
return h(Child, { count: this.$props.count }) return h(Child, { count: this.$props.count })
} }
} }
@ -317,7 +318,7 @@ describe('api: options', () => {
unmounted() { unmounted() {
calls.push('child onUnmounted') calls.push('child onUnmounted')
}, },
render() { render(this: any) {
return h('div', this.$props.count) return h('div', this.$props.count)
} }
} }
@ -375,7 +376,7 @@ describe('api: options', () => {
a: 1 a: 1
} }
}, },
created() { created(this: any) {
calls.push('mixinA created') calls.push('mixinA created')
expect(this.a).toBe(1) expect(this.a).toBe(1)
expect(this.b).toBe(2) expect(this.b).toBe(2)
@ -391,7 +392,7 @@ describe('api: options', () => {
b: 2 b: 2
} }
}, },
created() { created(this: any) {
calls.push('mixinB created') calls.push('mixinB created')
expect(this.a).toBe(1) expect(this.a).toBe(1)
expect(this.b).toBe(2) expect(this.b).toBe(2)
@ -408,7 +409,7 @@ describe('api: options', () => {
c: 3 c: 3
} }
}, },
created() { created(this: any) {
calls.push('comp created') calls.push('comp created')
expect(this.a).toBe(1) expect(this.a).toBe(1)
expect(this.b).toBe(2) expect(this.b).toBe(2)
@ -417,7 +418,7 @@ describe('api: options', () => {
mounted() { mounted() {
calls.push('comp mounted') calls.push('comp mounted')
}, },
render() { render(this: any) {
return `${this.a}${this.b}${this.c}` return `${this.a}${this.b}${this.c}`
} }
} }
@ -455,7 +456,7 @@ describe('api: options', () => {
mounted() { mounted() {
calls.push('comp') calls.push('comp')
}, },
render() { render(this: any) {
return `${this.a}${this.b}` return `${this.a}${this.b}`
} }
} }
@ -465,7 +466,7 @@ describe('api: options', () => {
}) })
test('accessing setup() state from options', async () => { test('accessing setup() state from options', async () => {
const Comp = { const Comp = createComponent({
setup() { setup() {
return { return {
count: ref(0) count: ref(0)
@ -473,11 +474,11 @@ describe('api: options', () => {
}, },
data() { data() {
return { return {
plusOne: this.count + 1 plusOne: (this as any).count + 1
} }
}, },
computed: { computed: {
plusTwo() { plusTwo(): number {
return this.count + 2 return this.count + 2
} }
}, },
@ -495,7 +496,7 @@ describe('api: options', () => {
`${this.count},${this.plusOne},${this.plusTwo}` `${this.count},${this.plusOne},${this.plusTwo}`
) )
} }
} })
const root = nodeOps.createElement('div') const root = nodeOps.createElement('div')
render(h(Comp), root) render(h(Comp), root)
expect(serializeInner(root)).toBe(`<div>0,1,2</div>`) expect(serializeInner(root)).toBe(`<div>0,1,2</div>`)

View File

@ -16,7 +16,7 @@ import {
describe('api: setup context', () => { describe('api: setup context', () => {
it('should expose return values to template render context', () => { it('should expose return values to template render context', () => {
const Comp = { const Comp = createComponent({
setup() { setup() {
return { return {
// ref should auto-unwrap // ref should auto-unwrap
@ -30,7 +30,7 @@ describe('api: setup context', () => {
render() { render() {
return `${this.ref} ${this.object.msg} ${this.value}` return `${this.ref} ${this.object.msg} ${this.value}`
} }
} })
expect(renderToString(h(Comp))).toMatch(`foo bar baz`) expect(renderToString(h(Comp))).toMatch(`foo bar baz`)
}) })

View File

@ -66,30 +66,20 @@ export interface LegacyOptions<
RawBindings, RawBindings,
D, D,
C extends ComputedOptions, C extends ComputedOptions,
M extends MethodOptions, M extends MethodOptions
ThisContext = ThisType<ComponentRenderProxy<Props, D, RawBindings, C, M>>
> { > {
el?: any el?: any
// state // state
data?: data?: D | ((this: ComponentRenderProxy<Props>) => D)
| D computed?: C
| (<This extends ComponentRenderProxy<Props, {}, RawBindings>>( methods?: M
this: This
) => D)
computed?: C & ThisContext
methods?: M & ThisContext
// TODO watch array // TODO watch array
watch?: Record< watch?: Record<
string, string,
string | WatchHandler | { handler: WatchHandler } & WatchOptions string | WatchHandler | { handler: WatchHandler } & WatchOptions
> & >
ThisContext provide?: Data | Function
provide?:
| Data
| (<This extends ComponentRenderProxy<Props, D, RawBindings, C, M>>(
this: This
) => any)
inject?: inject?:
| string[] | string[]
| Record< | Record<
@ -102,10 +92,8 @@ export interface LegacyOptions<
extends?: LegacyComponent extends?: LegacyComponent
// lifecycle // lifecycle
beforeCreate?(this: ComponentRenderProxy): void beforeCreate?(): void
created?<This extends ComponentRenderProxy<Props, D, RawBindings, C, M>>( created?(): void
this: This
): void
beforeMount?(): void beforeMount?(): void
mounted?(): void mounted?(): void
beforeUpdate?(): void beforeUpdate?(): void

View File

@ -18,14 +18,14 @@ export {
Ref, Ref,
ComputedRef, ComputedRef,
UnwrapRef, UnwrapRef,
ComputedOptions WritableComputedOptions
} from '@vue/reactivity' } from '@vue/reactivity'
import { import {
Ref, Ref,
computed as _computed, computed as _computed,
ComputedRef, ComputedRef,
ComputedOptions, WritableComputedOptions,
ReactiveEffect ReactiveEffect
} from '@vue/reactivity' } from '@vue/reactivity'
@ -40,7 +40,7 @@ export function recordEffect(effect: ReactiveEffect) {
} }
export function computed<T>(getter: () => T): ComputedRef<T> export function computed<T>(getter: () => T): ComputedRef<T>
export function computed<T>(options: ComputedOptions<T>): Ref<T> export function computed<T>(options: WritableComputedOptions<T>): Ref<T>
export function computed<T>(getterOrOptions: any) { export function computed<T>(getterOrOptions: any) {
const c = _computed(getterOrOptions) const c = _computed(getterOrOptions)
recordEffect(c.effect) recordEffect(c.effect)

View File

@ -37,8 +37,8 @@ export type Data = { [key: string]: unknown }
// in templates (as `this` in the render option) // in templates (as `this` in the render option)
export type ComponentRenderProxy< export type ComponentRenderProxy<
P = {}, P = {},
D = {},
B = {}, B = {},
D = {},
C = {}, C = {},
M = {}, M = {},
PublicProps = P PublicProps = P
@ -52,17 +52,11 @@ export type ComponentRenderProxy<
$parent: ComponentInstance | null $parent: ComponentInstance | null
$emit: (event: string, ...args: unknown[]) => void $emit: (event: string, ...args: unknown[]) => void
} & P & } & P &
D &
UnwrapRef<B> & UnwrapRef<B> &
D &
ExtracComputedReturns<C> & ExtracComputedReturns<C> &
M M
type RenderFunction<P = {}, D = {}, B = {}, C = {}, M = {}> = <
This extends ComponentRenderProxy<P, D, B, C, M>
>(
this: This
) => VNodeChild
interface ComponentOptionsBase< interface ComponentOptionsBase<
Props, Props,
RawBindings, RawBindings,
@ -71,47 +65,53 @@ interface ComponentOptionsBase<
M extends MethodOptions M extends MethodOptions
> extends LegacyOptions<Props, RawBindings, D, C, M> { > extends LegacyOptions<Props, RawBindings, D, C, M> {
setup?: ( setup?: (
this: null,
props: Props, props: Props,
ctx: SetupContext ctx: SetupContext
) => RawBindings | (() => VNodeChild) | void ) => RawBindings | (() => VNodeChild) | void
name?: string name?: string
template?: string template?: string
render?: RenderFunction<Props, D, RawBindings, C, M> // Note: we are intentionally using the signature-less `Function` type here
// since any type with signature will cause the whole inference to fail when
// the return expression contains reference to `this`.
// Luckily `render()` doesn't need any arguments nor does it care about return
// type.
render?: Function
components?: Record<string, Component> components?: Record<string, Component>
directives?: Record<string, Directive> directives?: Record<string, Directive>
} }
export interface ComponentOptionsWithoutProps< export type ComponentOptionsWithoutProps<
Props = {}, Props = {},
RawBindings = {}, RawBindings = {},
D = {}, D = {},
C extends ComputedOptions = {}, C extends ComputedOptions = {},
M extends MethodOptions = {} M extends MethodOptions = {}
> extends ComponentOptionsBase<Props, RawBindings, D, C, M> { > = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
props?: undefined props?: undefined
} } & ThisType<ComponentRenderProxy<Props, RawBindings, D, C, M>>
export interface ComponentOptionsWithArrayProps< export type ComponentOptionsWithArrayProps<
PropNames extends string = string, PropNames extends string = string,
RawBindings = {}, RawBindings = {},
D = {}, D = {},
C extends ComputedOptions = {}, C extends ComputedOptions = {},
M extends MethodOptions = {}, M extends MethodOptions = {},
Props = { [key in PropNames]?: unknown } Props = { [key in PropNames]?: unknown }
> extends ComponentOptionsBase<Props, RawBindings, D, C, M> { > = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
props: PropNames[] props: PropNames[]
} } & ThisType<ComponentRenderProxy<Props, RawBindings, D, C, M>>
export interface ComponentOptionsWithProps< export type ComponentOptionsWithProps<
PropsOptions = ComponentPropsOptions, PropsOptions = ComponentPropsOptions,
RawBindings = {}, RawBindings = {},
D = {}, D = {},
C extends ComputedOptions = {}, C extends ComputedOptions = {},
M extends MethodOptions = {}, M extends MethodOptions = {},
Props = ExtractPropTypes<PropsOptions> Props = ExtractPropTypes<PropsOptions>
> extends ComponentOptionsBase<Props, RawBindings, D, C, M> { > = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
props: PropsOptions props: PropsOptions
} } & ThisType<ComponentRenderProxy<Props, RawBindings, D, C, M>>
export type ComponentOptions = export type ComponentOptions =
| ComponentOptionsWithoutProps | ComponentOptionsWithoutProps
@ -150,6 +150,8 @@ interface SetupContext {
emit: ((event: string, ...args: unknown[]) => void) emit: ((event: string, ...args: unknown[]) => void)
} }
type RenderFunction = () => VNodeChild
export type ComponentInstance<P = Data, D = Data> = { export type ComponentInstance<P = Data, D = Data> = {
type: FunctionalComponent | ComponentOptions type: FunctionalComponent | ComponentOptions
parent: ComponentInstance | null parent: ComponentInstance | null
@ -159,7 +161,7 @@ export type ComponentInstance<P = Data, D = Data> = {
next: VNode | null next: VNode | null
subTree: VNode subTree: VNode
update: ReactiveEffect update: ReactiveEffect
render: RenderFunction<P, D> | null render: RenderFunction | null
effects: ReactiveEffect[] | null effects: ReactiveEffect[] | null
provides: Data provides: Data
@ -211,7 +213,7 @@ export function createComponent<
>( >(
options: ComponentOptionsWithoutProps<Props, RawBindings, D, C, M> options: ComponentOptionsWithoutProps<Props, RawBindings, D, C, M>
): { ): {
new (): ComponentRenderProxy<Props, D, RawBindings, C, M> new (): ComponentRenderProxy<Props, RawBindings, D, C, M>
} }
// overload 3: object format with array props declaration // overload 3: object format with array props declaration
// props inferred as { [key in PropNames]?: unknown } // props inferred as { [key in PropNames]?: unknown }
@ -227,8 +229,8 @@ export function createComponent<
): { ): {
new (): ComponentRenderProxy< new (): ComponentRenderProxy<
{ [key in PropNames]?: unknown }, { [key in PropNames]?: unknown },
D,
RawBindings, RawBindings,
D,
C, C,
M M
> >
@ -247,8 +249,8 @@ export function createComponent<
// for Vetur and TSX support // for Vetur and TSX support
new (): ComponentRenderProxy< new (): ComponentRenderProxy<
ExtractPropTypes<PropsOptions>, ExtractPropTypes<PropsOptions>,
D,
RawBindings, RawBindings,
D,
C, C,
M, M,
ExtractPropTypes<PropsOptions, false> ExtractPropTypes<PropsOptions, false>

View File

@ -11,10 +11,10 @@ import { Ref } from '@vue/reactivity'
import { RawSlots } from './componentSlots' import { RawSlots } from './componentSlots'
import { import {
FunctionalComponent, FunctionalComponent,
ComponentOptions,
ComponentOptionsWithoutProps, ComponentOptionsWithoutProps,
ComponentOptionsWithArrayProps, ComponentOptionsWithArrayProps,
ComponentOptionsWithProps ComponentOptionsWithProps,
ComponentOptions
} from './component' } from './component'
import { ExtractPropTypes } from './componentProps' import { ExtractPropTypes } from './componentProps'
@ -57,7 +57,7 @@ interface Props {
[Symbol.iterator]?: never [Symbol.iterator]?: never
} }
type Children = string | number | VNodeChildren type Children = string | number | boolean | VNodeChildren | (() => any)
// fake constructor type returned from `createComponent` // fake constructor type returned from `createComponent`
interface Constructor<P = any> { interface Constructor<P = any> {

View File

@ -28,7 +28,7 @@ export type VNodeTypes =
| typeof Text | typeof Text
| typeof Empty | typeof Empty
type VNodeChildAtom = VNode | string | number | null | void type VNodeChildAtom = VNode | string | number | boolean | null | void
export interface VNodeChildren extends Array<VNodeChildren | VNodeChildAtom> {} export interface VNodeChildren extends Array<VNodeChildren | VNodeChildAtom> {}
export type VNodeChild = VNodeChildAtom | VNodeChildren export type VNodeChild = VNodeChildAtom | VNodeChildren