From 9b90e673e8bc4b5ed66d4af69761355c6a12dd46 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 5 Sep 2019 16:09:30 -0400 Subject: [PATCH] wip: improve options typing --- packages/reactivity/src/computed.ts | 2 +- .../__tests__/apiCreateComponent.spec.tsx | 50 +++++++- packages/runtime-core/src/apiOptions.ts | 72 +++++++++--- packages/runtime-core/src/apiWatch.ts | 2 +- packages/runtime-core/src/component.ts | 110 +++++++++++++----- tsconfig.json | 1 + 6 files changed, 192 insertions(+), 45 deletions(-) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index d795564404..5504ac79bb 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -7,7 +7,7 @@ export interface ComputedRef { readonly effect: ReactiveEffect } -export interface ComputedOptions { +export interface ComputedOptions { get: () => T set: (v: T) => void } diff --git a/packages/runtime-core/__tests__/apiCreateComponent.spec.tsx b/packages/runtime-core/__tests__/apiCreateComponent.spec.tsx index a2775d4c54..991f826436 100644 --- a/packages/runtime-core/__tests__/apiCreateComponent.spec.tsx +++ b/packages/runtime-core/__tests__/apiCreateComponent.spec.tsx @@ -1,6 +1,7 @@ -import { createComponent } from '../src/component' +import { createComponent, ComponentRenderProxy } from '../src/component' import { ref } from '@vue/reactivity' import { PropType } from '../src/componentProps' +import { h } from '../src/h' // mock React just for TSX testing purposes const React = { @@ -55,6 +56,7 @@ test('createComponent type inference', () => { this.d.e.slice() this.cc && this.cc.push('hoo') this.dd.push('dd') + // return h('div', this.bb) } }) // test TSX props inference @@ -71,9 +73,9 @@ test('type inference w/ optional props declaration', () => { }, render() { this.$props.msg - this.$data.a * 2 this.msg this.a * 2 + // return h('div', this.msg) } }) ;() @@ -99,7 +101,6 @@ test('type inference w/ array props declaration', () => { render() { this.$props.a this.$props.b - this.$data.c this.a this.b this.c @@ -107,3 +108,46 @@ test('type inference w/ array props declaration', () => { }) ;() }) + +test('with legacy options', () => { + createComponent({ + props: { a: Number }, + setup() { + return { + b: 123 + } + }, + data() { + this.a + this.b + return { + c: 234 + } + }, + 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 + } + } + }) +}) diff --git a/packages/runtime-core/src/apiOptions.ts b/packages/runtime-core/src/apiOptions.ts index 7ed2590a7b..4c5b517e38 100644 --- a/packages/runtime-core/src/apiOptions.ts +++ b/packages/runtime-core/src/apiOptions.ts @@ -3,7 +3,8 @@ import { Data, ComponentOptions, currentRenderingInstance, - currentInstance + currentInstance, + ComponentRenderProxy } from './component' import { isFunction, @@ -15,8 +16,8 @@ import { capitalize, camelize } from '@vue/shared' -import { computed, ComputedOptions } from './apiReactivity' -import { watch, WatchOptions } from './apiWatch' +import { computed } from './apiReactivity' +import { watch, WatchOptions, CleanupRegistrator } from './apiWatch' import { provide, inject } from './apiInject' import { onBeforeMount, @@ -34,20 +35,61 @@ import { warn } from './warning' // TODO legacy component definition also supports constructors with .options type LegacyComponent = ComponentOptions +export interface ComputedOptions { + [key: string]: + | Function + | { + get: Function + set: Function + } +} + +export interface MethodOptions { + [key: string]: Function +} + +export type ExtracComputedReturns = { + [key in keyof T]: T[key] extends { get: Function } + ? ReturnType + : ReturnType +} + +type WatchHandler = ( + val: any, + oldVal: any, + onCleanup: CleanupRegistrator +) => void + // TODO type inference for these options -export interface LegacyOptions { +export interface LegacyOptions< + Props, + RawBindings, + D, + C extends ComputedOptions, + M extends MethodOptions, + ThisContext = ThisType> +> { el?: any // state - data?: Data | (() => Data) - computed?: Record any) | ComputedOptions> - methods?: Record + data?: + | D + | (>( + this: This + ) => D) + computed?: C & ThisContext + methods?: M & ThisContext // TODO watch array watch?: Record< string, - string | Function | { handler: Function } & WatchOptions - > - provide?: Data | (() => Data) + string | WatchHandler | { handler: WatchHandler } & WatchOptions + > & + ThisContext + provide?: + | Data + | (>( + this: This + ) => any) inject?: | string[] | Record< @@ -60,8 +102,10 @@ export interface LegacyOptions { extends?: LegacyComponent // lifecycle - beforeCreate?(): void - created?(): void + beforeCreate?(this: ComponentRenderProxy): void + created?>( + this: This + ): void beforeMount?(): void mounted?(): void beforeUpdate?(): void @@ -138,7 +182,7 @@ export function applyOptions( } if (computedOptions) { for (const key in computedOptions) { - const opt = computedOptions[key] + const opt = (computedOptions as ComputedOptions)[key] data[key] = isFunction(opt) ? computed(opt.bind(ctx)) : computed({ @@ -149,7 +193,7 @@ export function applyOptions( } if (methods) { for (const key in methods) { - data[key] = methods[key].bind(ctx) + data[key] = (methods as MethodOptions)[key].bind(ctx) } } if (watchOptions) { diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 2b85b3838d..3c14d915f7 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -32,7 +32,7 @@ type MapSources = { [K in keyof T]: T[K] extends WatcherSource ? V : never } -type CleanupRegistrator = (invalidate: () => void) => void +export type CleanupRegistrator = (invalidate: () => void) => void type SimpleEffect = (onCleanup: CleanupRegistrator) => void diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index afd9a98a0e..4a897ccdee 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -22,14 +22,28 @@ import { } from './errorHandling' import { AppContext, createAppContext } from './apiApp' import { Directive } from './directives' -import { applyOptions, LegacyOptions, resolveAsset } from './apiOptions' +import { + applyOptions, + LegacyOptions, + resolveAsset, + ComputedOptions, + MethodOptions, + ExtracComputedReturns +} from './apiOptions' export type Data = { [key: string]: unknown } // public properties exposed on the proxy, which is used as the render context // in templates (as `this` in the render option) -export type ComponentRenderProxy

= { - $data: S +export type ComponentRenderProxy< + P = {}, + D = {}, + B = {}, + C = {}, + M = {}, + PublicProps = P +> = { + $data: D $props: PublicProps $attrs: Data $refs: Data @@ -38,50 +52,70 @@ export type ComponentRenderProxy

= { $parent: ComponentInstance | null $emit: (event: string, ...args: unknown[]) => void } & P & - S + D & + UnwrapRef & + ExtracComputedReturns & + M -type RenderFunction = < - Bindings extends UnwrapRef +type RenderFunction

= < + This extends ComponentRenderProxy >( - this: ComponentRenderProxy + this: This ) => VNodeChild -interface ComponentOptionsBase extends LegacyOptions { +interface ComponentOptionsBase< + Props, + RawBindings, + D, + C extends ComputedOptions, + M extends MethodOptions +> extends LegacyOptions { setup?: ( props: Props, ctx: SetupContext ) => RawBindings | (() => VNodeChild) | void name?: string template?: string - render?: RenderFunction + render?: RenderFunction components?: Record directives?: Record } -export interface ComponentOptionsWithoutProps - extends ComponentOptionsBase { +export interface ComponentOptionsWithoutProps< + Props = {}, + RawBindings = {}, + D = {}, + C extends ComputedOptions = {}, + M extends MethodOptions = {} +> extends ComponentOptionsBase { props?: undefined } export interface ComponentOptionsWithArrayProps< PropNames extends string = string, RawBindings = {}, + D = {}, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, Props = { [key in PropNames]?: unknown } -> extends ComponentOptionsBase { +> extends ComponentOptionsBase { props: PropNames[] } export interface ComponentOptionsWithProps< PropsOptions = ComponentPropsOptions, RawBindings = {}, + D = {}, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, Props = ExtractPropTypes -> extends ComponentOptionsBase { +> extends ComponentOptionsBase { props: PropsOptions } export type ComponentOptions = - | ComponentOptionsWithProps | ComponentOptionsWithoutProps + | ComponentOptionsWithProps | ComponentOptionsWithArrayProps export interface FunctionalComponent

{ @@ -116,7 +150,7 @@ interface SetupContext { emit: ((event: string, ...args: unknown[]) => void) } -export type ComponentInstance

= { +export type ComponentInstance

= { type: FunctionalComponent | ComponentOptions parent: ComponentInstance | null appContext: AppContext @@ -125,7 +159,7 @@ export type ComponentInstance

= { next: VNode | null subTree: VNode update: ReactiveEffect - render: RenderFunction | null + render: RenderFunction | null effects: ReactiveEffect[] | null provides: Data @@ -133,7 +167,7 @@ export type ComponentInstance

= { directives: Record // the rest are only for stateful components - data: S + data: D props: P renderProxy: ComponentRenderProxy | null propsProxy: P | null @@ -168,31 +202,55 @@ export function createComponent( // overload 2: object format with no props // (uses user defined props interface) // return type is for Vetur and TSX support -export function createComponent( - options: ComponentOptionsWithoutProps +export function createComponent< + Props, + RawBindings, + D, + C extends ComputedOptions = {}, + M extends MethodOptions = {} +>( + options: ComponentOptionsWithoutProps ): { - new (): ComponentRenderProxy> + new (): ComponentRenderProxy } // overload 3: object format with array props declaration // props inferred as { [key in PropNames]?: unknown } // return type is for Vetur and TSX support -export function createComponent( - options: ComponentOptionsWithArrayProps +export function createComponent< + PropNames extends string, + RawBindings, + D, + C extends ComputedOptions = {}, + M extends MethodOptions = {} +>( + options: ComponentOptionsWithArrayProps ): { new (): ComponentRenderProxy< { [key in PropNames]?: unknown }, - UnwrapRef + D, + RawBindings, + C, + M > } // overload 4: object format with object props declaration // see `ExtractPropTypes` in ./componentProps.ts -export function createComponent( - options: ComponentOptionsWithProps +export function createComponent< + PropsOptions, + RawBindings, + D, + C extends ComputedOptions = {}, + M extends MethodOptions = {} +>( + options: ComponentOptionsWithProps ): { // for Vetur and TSX support new (): ComponentRenderProxy< ExtractPropTypes, - UnwrapRef, + D, + RawBindings, + C, + M, ExtractPropTypes > } diff --git a/tsconfig.json b/tsconfig.json index b3b5f474e7..46cb2306c7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "noUnusedLocals": true, "strictNullChecks": true, "noImplicitAny": true, + "noImplicitThis": true, "experimentalDecorators": true, "esModuleInterop": true, "removeComments": false, -- 2.47.3