From 69344ff1ae724beb648c34ede8050b3b70ddf4b7 Mon Sep 17 00:00:00 2001 From: Amour1688 Date: Tue, 20 Jul 2021 06:22:19 +0800 Subject: [PATCH] feat(types): map declared emits to onXXX props in inferred prop types (#3926) --- .prettierrc | 2 + package.json | 2 +- .../runtime-core/src/apiDefineComponent.ts | 6 +- packages/runtime-core/src/componentEmits.ts | 32 ++++++--- packages/runtime-core/src/componentOptions.ts | 67 ++++++++++--------- test-dts/defineComponent.test-d.tsx | 27 ++++++-- yarn.lock | 8 +-- 7 files changed, 93 insertions(+), 51 deletions(-) diff --git a/.prettierrc b/.prettierrc index f5a1bdcdd2d..ef93d94821a 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,5 @@ semi: false singleQuote: true printWidth: 80 +trailingComma: 'none' +arrowParens: 'avoid' diff --git a/package.json b/package.json index c8ca74c556e..2a220c57abf 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "lint-staged": "^10.2.10", "minimist": "^1.2.0", "npm-run-all": "^4.1.5", - "prettier": "~1.14.0", + "prettier": "^2.3.1", "puppeteer": "^10.0.0", "rollup": "~2.38.5", "rollup-plugin-node-builtins": "^2.1.2", diff --git a/packages/runtime-core/src/apiDefineComponent.ts b/packages/runtime-core/src/apiDefineComponent.ts index 333d18a30f3..1560003bcd4 100644 --- a/packages/runtime-core/src/apiDefineComponent.ts +++ b/packages/runtime-core/src/apiDefineComponent.ts @@ -18,7 +18,7 @@ import { ComponentPropsOptions, ExtractDefaultPropTypes } from './componentProps' -import { EmitsOptions } from './componentEmits' +import { EmitsOptions, EmitsToProps } from './componentEmits' import { isFunction } from '@vue/shared' import { VNodeProps } from './vnode' import { @@ -41,7 +41,7 @@ export type DefineComponent< E extends EmitsOptions = Record, EE extends string = string, PP = PublicProps, - Props = Readonly>, + Props = Readonly> & EmitsToProps, Defaults = ExtractDefaultPropTypes > = ComponentPublicInstanceConstructor< CreateComponentPublicInstance< @@ -102,7 +102,7 @@ export function defineComponent< EE extends string = string >( options: ComponentOptionsWithoutProps< - Props, + Props & EmitsToProps, RawBindings, D, C, diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts index 52900ea3176..b98f8c34e84 100644 --- a/packages/runtime-core/src/componentEmits.ts +++ b/packages/runtime-core/src/componentEmits.ts @@ -31,22 +31,38 @@ export type ObjectEmitsOptions = Record< string, ((...args: any[]) => any) | null > + export type EmitsOptions = ObjectEmitsOptions | string[] +export type EmitsToProps = T extends string[] + ? { + [K in string & `on${Capitalize}`]?: (...args: any[]) => any + } + : T extends ObjectEmitsOptions + ? { + [K in string & + `on${Capitalize}`]?: K extends `on${infer C}` + ? T[Uncapitalize] extends null + ? (...args: any[]) => any + : T[Uncapitalize] + : never + } + : {} + export type EmitFn< Options = ObjectEmitsOptions, Event extends keyof Options = keyof Options > = Options extends Array ? (event: V, ...args: any[]) => void : {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function - ? (event: string, ...args: any[]) => void - : UnionToIntersection< - { - [key in Event]: Options[key] extends ((...args: infer Args) => any) - ? (event: key, ...args: Args) => void - : (event: key, ...args: any[]) => void - }[Event] - > + ? (event: string, ...args: any[]) => void + : UnionToIntersection< + { + [key in Event]: Options[key] extends (...args: infer Args) => any + ? (event: key, ...args: Args) => void + : (event: key, ...args: any[]) => void + }[Event] + > export function emit( instance: ComponentInternalInstance, diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index e8cc5f75d67..bffb215cd5c 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -51,7 +51,7 @@ import { ExtractPropTypes, ExtractDefaultPropTypes } from './componentProps' -import { EmitsOptions } from './componentEmits' +import { EmitsOptions, EmitsToProps } from './componentEmits' import { Directive } from './directives' import { CreateComponentPublicInstance, @@ -91,16 +91,18 @@ export interface ComponentCustomOptions {} export type RenderFunction = () => VNodeChild type ExtractOptionProp = T extends ComponentOptionsBase< - infer P, - any, - any, - any, - any, - any, - any, - any + infer P, // Props + any, // RawBindings + any, // D + any, // C + any, // M + any, // Mixin + any, // Extends + any // EmitsOptions > - ? unknown extends P ? {} : P + ? unknown extends P + ? {} + : P : {} export interface ComponentOptionsBase< @@ -114,8 +116,7 @@ export interface ComponentOptionsBase< E extends EmitsOptions, EE extends string = string, Defaults = {} -> - extends LegacyOptions, +> extends LegacyOptions, ComponentInternalOptions, ComponentCustomOptions { setup?: ( @@ -220,9 +221,10 @@ export type ComponentOptionsWithoutProps< Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, E extends EmitsOptions = EmitsOptions, - EE extends string = string + EE extends string = string, + PE = Props & EmitsToProps > = ComponentOptionsBase< - Props, + PE, RawBindings, D, C, @@ -235,7 +237,7 @@ export type ComponentOptionsWithoutProps< > & { props?: undefined } & ThisType< - CreateComponentPublicInstance<{}, RawBindings, D, C, M, Mixin, Extends, E> + CreateComponentPublicInstance > export type ComponentOptionsWithArrayProps< @@ -248,7 +250,7 @@ export type ComponentOptionsWithArrayProps< Extends extends ComponentOptionsMixin = ComponentOptionsMixin, E extends EmitsOptions = EmitsOptions, EE extends string = string, - Props = Readonly<{ [key in PropNames]?: any }> + Props = Readonly<{ [key in PropNames]?: any }> & EmitsToProps > = ComponentOptionsBase< Props, RawBindings, @@ -285,7 +287,7 @@ export type ComponentOptionsWithObjectProps< Extends extends ComponentOptionsMixin = ComponentOptionsMixin, E extends EmitsOptions = EmitsOptions, EE extends string = string, - Props = Readonly>, + Props = Readonly> & EmitsToProps, Defaults = ExtractDefaultPropTypes > = ComponentOptionsBase< Props, @@ -365,7 +367,9 @@ export interface MethodOptions { export type ExtractComputedReturns = { [key in keyof T]: T[key] extends { get: (...args: any[]) => infer TReturn } ? TReturn - : T[key] extends (...args: any[]) => infer TReturn ? TReturn : never + : T[key] extends (...args: any[]) => infer TReturn + ? TReturn + : never } export type ObjectWatchOptionItem = { @@ -471,7 +475,7 @@ interface LegacyOptions< __differentiator?: keyof D | keyof C | keyof M } -type MergedHook void)> = T | T[] +type MergedHook void> = T | T[] export type MergedComponentOptions = ComponentOptions & MergedComponentOptionsOverride @@ -679,8 +683,8 @@ export function applyOptions(instance: ComponentInternalInstance) { const get = isFunction(opt) ? opt.bind(publicThis, publicThis) : isFunction(opt.get) - ? opt.get.bind(publicThis, publicThis) - : NOOP + ? opt.get.bind(publicThis, publicThis) + : NOOP if (__DEV__ && get === NOOP) { warn(`Computed property "${key}" has no getter.`) } @@ -688,12 +692,12 @@ export function applyOptions(instance: ComponentInternalInstance) { !isFunction(opt) && isFunction(opt.set) ? opt.set.bind(publicThis) : __DEV__ - ? () => { - warn( - `Write operation failed: computed property "${key}" is readonly.` - ) - } - : NOOP + ? () => { + warn( + `Write operation failed: computed property "${key}" is readonly.` + ) + } + : NOOP const c = computed({ get, set @@ -1006,10 +1010,11 @@ function mergeDataFn(to: any, from: any) { return from } return function mergedDataFn(this: ComponentPublicInstance) { - return (__COMPAT__ && - isCompatEnabled(DeprecationTypes.OPTIONS_DATA_MERGE, null) - ? deepMergeData - : extend)( + return ( + __COMPAT__ && isCompatEnabled(DeprecationTypes.OPTIONS_DATA_MERGE, null) + ? deepMergeData + : extend + )( isFunction(to) ? to.call(this, this) : to, isFunction(from) ? from.call(this, this) : from ) diff --git a/test-dts/defineComponent.test-d.tsx b/test-dts/defineComponent.test-d.tsx index 3f59bd6ad39..dba11f20b9b 100644 --- a/test-dts/defineComponent.test-d.tsx +++ b/test-dts/defineComponent.test-d.tsx @@ -469,6 +469,7 @@ describe('type inference w/ options API', () => { describe('with mixins', () => { const MixinA = defineComponent({ + emits: ['bar'], props: { aP1: { type: String, @@ -523,6 +524,7 @@ describe('with mixins', () => { }) const MyComponent = defineComponent({ mixins: [MixinA, MixinB, MixinC, MixinD], + emits: ['click'], props: { // required should make property non-void z: { @@ -552,6 +554,9 @@ describe('with mixins', () => { setup(props) { expectType(props.z) // props + expectType<((...args: any[]) => any) | undefined>(props.onClick) + // from Base + expectType<((...args: any[]) => any) | undefined>(props.onBar) expectType(props.aP1) expectType(props.aP2) expectType(props.bP1) @@ -561,6 +566,9 @@ describe('with mixins', () => { render() { const props = this.$props // props + expectType<((...args: any[]) => any) | undefined>(props.onClick) + // from Base + expectType<((...args: any[]) => any) | undefined>(props.onBar) expectType(props.aP1) expectType(props.aP2) expectType(props.bP1) @@ -688,6 +696,7 @@ describe('with extends', () => { describe('extends with mixins', () => { const Mixin = defineComponent({ + emits: ['bar'], props: { mP1: { type: String, @@ -706,6 +715,7 @@ describe('extends with mixins', () => { } }) const Base = defineComponent({ + emits: ['foo'], props: { p1: Boolean, p2: { @@ -731,6 +741,7 @@ describe('extends with mixins', () => { const MyComponent = defineComponent({ extends: Base, mixins: [Mixin], + emits: ['click'], props: { // required should make property non-void z: { @@ -741,6 +752,11 @@ describe('extends with mixins', () => { render() { const props = this.$props // props + expectType<((...args: any[]) => any) | undefined>(props.onClick) + // from Mixin + expectType<((...args: any[]) => any) | undefined>(props.onBar) + // from Base + expectType<((...args: any[]) => any) | undefined>(props.onFoo) expectType(props.p1) expectType(props.p2) expectType(props.z) @@ -879,6 +895,8 @@ describe('emits', () => { input: (b: string) => b.length > 1 }, setup(props, { emit }) { + expectType<((n: number) => boolean) | undefined>(props.onClick) + expectType<((b: string) => boolean) | undefined>(props.onInput) emit('click', 1) emit('input', 'foo') // @ts-expect-error @@ -931,6 +949,8 @@ describe('emits', () => { defineComponent({ emits: ['foo', 'bar'], setup(props, { emit }) { + expectType<((...args: any[]) => any) | undefined>(props.onFoo) + expectType<((...args: any[]) => any) | undefined>(props.onBar) emit('foo') emit('foo', 123) emit('bar') @@ -972,10 +992,9 @@ describe('emits', () => { }) describe('componentOptions setup should be `SetupContext`', () => { - expect({} as ( - props: Record, - ctx: SetupContext - ) => any) + expect( + {} as (props: Record, ctx: SetupContext) => any + ) }) describe('extract instance type', () => { diff --git a/yarn.lock b/yarn.lock index f66481b73ee..8f9415ed230 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5630,10 +5630,10 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prettier@~1.14.0: - version "1.14.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.3.tgz#90238dd4c0684b7edce5f83b0fb7328e48bd0895" - integrity sha512-qZDVnCrnpsRJJq5nSsiHCE3BYMED2OtsI+cmzIzF1QIfqm5ALf8tEJcO27zV1gKNKRPdhjO0dNWnrzssDQ1tFg== +prettier@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6" + integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA== pretty-format@^26.0.0, pretty-format@^26.6.2: version "26.6.2"