feat(types): map declared emits to onXXX props in inferred prop types (#3926)

This commit is contained in:
Amour1688 2021-07-20 06:22:19 +08:00 committed by GitHub
parent 35cc7b0d31
commit 69344ff1ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 93 additions and 51 deletions

View File

@ -1,3 +1,5 @@
semi: false semi: false
singleQuote: true singleQuote: true
printWidth: 80 printWidth: 80
trailingComma: 'none'
arrowParens: 'avoid'

View File

@ -68,7 +68,7 @@
"lint-staged": "^10.2.10", "lint-staged": "^10.2.10",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "~1.14.0", "prettier": "^2.3.1",
"puppeteer": "^10.0.0", "puppeteer": "^10.0.0",
"rollup": "~2.38.5", "rollup": "~2.38.5",
"rollup-plugin-node-builtins": "^2.1.2", "rollup-plugin-node-builtins": "^2.1.2",

View File

@ -18,7 +18,7 @@ import {
ComponentPropsOptions, ComponentPropsOptions,
ExtractDefaultPropTypes ExtractDefaultPropTypes
} from './componentProps' } from './componentProps'
import { EmitsOptions } from './componentEmits' import { EmitsOptions, EmitsToProps } from './componentEmits'
import { isFunction } from '@vue/shared' import { isFunction } from '@vue/shared'
import { VNodeProps } from './vnode' import { VNodeProps } from './vnode'
import { import {
@ -41,7 +41,7 @@ export type DefineComponent<
E extends EmitsOptions = Record<string, any>, E extends EmitsOptions = Record<string, any>,
EE extends string = string, EE extends string = string,
PP = PublicProps, PP = PublicProps,
Props = Readonly<ExtractPropTypes<PropsOrPropOptions>>, Props = Readonly<ExtractPropTypes<PropsOrPropOptions>> & EmitsToProps<E>,
Defaults = ExtractDefaultPropTypes<PropsOrPropOptions> Defaults = ExtractDefaultPropTypes<PropsOrPropOptions>
> = ComponentPublicInstanceConstructor< > = ComponentPublicInstanceConstructor<
CreateComponentPublicInstance< CreateComponentPublicInstance<
@ -102,7 +102,7 @@ export function defineComponent<
EE extends string = string EE extends string = string
>( >(
options: ComponentOptionsWithoutProps< options: ComponentOptionsWithoutProps<
Props, Props & EmitsToProps<E>,
RawBindings, RawBindings,
D, D,
C, C,

View File

@ -31,22 +31,38 @@ export type ObjectEmitsOptions = Record<
string, string,
((...args: any[]) => any) | null ((...args: any[]) => any) | null
> >
export type EmitsOptions = ObjectEmitsOptions | string[] export type EmitsOptions = ObjectEmitsOptions | string[]
export type EmitsToProps<T extends EmitsOptions> = T extends string[]
? {
[K in string & `on${Capitalize<T[number]>}`]?: (...args: any[]) => any
}
: T extends ObjectEmitsOptions
? {
[K in string &
`on${Capitalize<string & keyof T>}`]?: K extends `on${infer C}`
? T[Uncapitalize<C>] extends null
? (...args: any[]) => any
: T[Uncapitalize<C>]
: never
}
: {}
export type EmitFn< export type EmitFn<
Options = ObjectEmitsOptions, Options = ObjectEmitsOptions,
Event extends keyof Options = keyof Options Event extends keyof Options = keyof Options
> = Options extends Array<infer V> > = Options extends Array<infer V>
? (event: V, ...args: any[]) => void ? (event: V, ...args: any[]) => void
: {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function : {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function
? (event: string, ...args: any[]) => void ? (event: string, ...args: any[]) => void
: UnionToIntersection< : UnionToIntersection<
{ {
[key in Event]: Options[key] extends ((...args: infer Args) => any) [key in Event]: Options[key] extends (...args: infer Args) => any
? (event: key, ...args: Args) => void ? (event: key, ...args: Args) => void
: (event: key, ...args: any[]) => void : (event: key, ...args: any[]) => void
}[Event] }[Event]
> >
export function emit( export function emit(
instance: ComponentInternalInstance, instance: ComponentInternalInstance,

View File

@ -51,7 +51,7 @@ import {
ExtractPropTypes, ExtractPropTypes,
ExtractDefaultPropTypes ExtractDefaultPropTypes
} from './componentProps' } from './componentProps'
import { EmitsOptions } from './componentEmits' import { EmitsOptions, EmitsToProps } from './componentEmits'
import { Directive } from './directives' import { Directive } from './directives'
import { import {
CreateComponentPublicInstance, CreateComponentPublicInstance,
@ -91,16 +91,18 @@ export interface ComponentCustomOptions {}
export type RenderFunction = () => VNodeChild export type RenderFunction = () => VNodeChild
type ExtractOptionProp<T> = T extends ComponentOptionsBase< type ExtractOptionProp<T> = T extends ComponentOptionsBase<
infer P, infer P, // Props
any, any, // RawBindings
any, any, // D
any, any, // C
any, any, // M
any, any, // Mixin
any, any, // Extends
any any // EmitsOptions
> >
? unknown extends P ? {} : P ? unknown extends P
? {}
: P
: {} : {}
export interface ComponentOptionsBase< export interface ComponentOptionsBase<
@ -114,8 +116,7 @@ export interface ComponentOptionsBase<
E extends EmitsOptions, E extends EmitsOptions,
EE extends string = string, EE extends string = string,
Defaults = {} Defaults = {}
> > extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
ComponentInternalOptions, ComponentInternalOptions,
ComponentCustomOptions { ComponentCustomOptions {
setup?: ( setup?: (
@ -220,9 +221,10 @@ export type ComponentOptionsWithoutProps<
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions, E extends EmitsOptions = EmitsOptions,
EE extends string = string EE extends string = string,
PE = Props & EmitsToProps<E>
> = ComponentOptionsBase< > = ComponentOptionsBase<
Props, PE,
RawBindings, RawBindings,
D, D,
C, C,
@ -235,7 +237,7 @@ export type ComponentOptionsWithoutProps<
> & { > & {
props?: undefined props?: undefined
} & ThisType< } & ThisType<
CreateComponentPublicInstance<{}, RawBindings, D, C, M, Mixin, Extends, E> CreateComponentPublicInstance<PE, RawBindings, D, C, M, Mixin, Extends, E>
> >
export type ComponentOptionsWithArrayProps< export type ComponentOptionsWithArrayProps<
@ -248,7 +250,7 @@ export type ComponentOptionsWithArrayProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions, E extends EmitsOptions = EmitsOptions,
EE extends string = string, EE extends string = string,
Props = Readonly<{ [key in PropNames]?: any }> Props = Readonly<{ [key in PropNames]?: any }> & EmitsToProps<E>
> = ComponentOptionsBase< > = ComponentOptionsBase<
Props, Props,
RawBindings, RawBindings,
@ -285,7 +287,7 @@ export type ComponentOptionsWithObjectProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions, E extends EmitsOptions = EmitsOptions,
EE extends string = string, EE extends string = string,
Props = Readonly<ExtractPropTypes<PropsOptions>>, Props = Readonly<ExtractPropTypes<PropsOptions>> & EmitsToProps<E>,
Defaults = ExtractDefaultPropTypes<PropsOptions> Defaults = ExtractDefaultPropTypes<PropsOptions>
> = ComponentOptionsBase< > = ComponentOptionsBase<
Props, Props,
@ -365,7 +367,9 @@ export interface MethodOptions {
export type ExtractComputedReturns<T extends any> = { export type ExtractComputedReturns<T extends any> = {
[key in keyof T]: T[key] extends { get: (...args: any[]) => infer TReturn } [key in keyof T]: T[key] extends { get: (...args: any[]) => infer TReturn }
? TReturn ? TReturn
: T[key] extends (...args: any[]) => infer TReturn ? TReturn : never : T[key] extends (...args: any[]) => infer TReturn
? TReturn
: never
} }
export type ObjectWatchOptionItem = { export type ObjectWatchOptionItem = {
@ -471,7 +475,7 @@ interface LegacyOptions<
__differentiator?: keyof D | keyof C | keyof M __differentiator?: keyof D | keyof C | keyof M
} }
type MergedHook<T = (() => void)> = T | T[] type MergedHook<T = () => void> = T | T[]
export type MergedComponentOptions = ComponentOptions & export type MergedComponentOptions = ComponentOptions &
MergedComponentOptionsOverride MergedComponentOptionsOverride
@ -679,8 +683,8 @@ export function applyOptions(instance: ComponentInternalInstance) {
const get = isFunction(opt) const get = isFunction(opt)
? opt.bind(publicThis, publicThis) ? opt.bind(publicThis, publicThis)
: isFunction(opt.get) : isFunction(opt.get)
? opt.get.bind(publicThis, publicThis) ? opt.get.bind(publicThis, publicThis)
: NOOP : NOOP
if (__DEV__ && get === NOOP) { if (__DEV__ && get === NOOP) {
warn(`Computed property "${key}" has no getter.`) warn(`Computed property "${key}" has no getter.`)
} }
@ -688,12 +692,12 @@ export function applyOptions(instance: ComponentInternalInstance) {
!isFunction(opt) && isFunction(opt.set) !isFunction(opt) && isFunction(opt.set)
? opt.set.bind(publicThis) ? opt.set.bind(publicThis)
: __DEV__ : __DEV__
? () => { ? () => {
warn( warn(
`Write operation failed: computed property "${key}" is readonly.` `Write operation failed: computed property "${key}" is readonly.`
) )
} }
: NOOP : NOOP
const c = computed({ const c = computed({
get, get,
set set
@ -1006,10 +1010,11 @@ function mergeDataFn(to: any, from: any) {
return from return from
} }
return function mergedDataFn(this: ComponentPublicInstance) { return function mergedDataFn(this: ComponentPublicInstance) {
return (__COMPAT__ && return (
isCompatEnabled(DeprecationTypes.OPTIONS_DATA_MERGE, null) __COMPAT__ && isCompatEnabled(DeprecationTypes.OPTIONS_DATA_MERGE, null)
? deepMergeData ? deepMergeData
: extend)( : extend
)(
isFunction(to) ? to.call(this, this) : to, isFunction(to) ? to.call(this, this) : to,
isFunction(from) ? from.call(this, this) : from isFunction(from) ? from.call(this, this) : from
) )

View File

@ -469,6 +469,7 @@ describe('type inference w/ options API', () => {
describe('with mixins', () => { describe('with mixins', () => {
const MixinA = defineComponent({ const MixinA = defineComponent({
emits: ['bar'],
props: { props: {
aP1: { aP1: {
type: String, type: String,
@ -523,6 +524,7 @@ describe('with mixins', () => {
}) })
const MyComponent = defineComponent({ const MyComponent = defineComponent({
mixins: [MixinA, MixinB, MixinC, MixinD], mixins: [MixinA, MixinB, MixinC, MixinD],
emits: ['click'],
props: { props: {
// required should make property non-void // required should make property non-void
z: { z: {
@ -552,6 +554,9 @@ describe('with mixins', () => {
setup(props) { setup(props) {
expectType<string>(props.z) expectType<string>(props.z)
// props // props
expectType<((...args: any[]) => any) | undefined>(props.onClick)
// from Base
expectType<((...args: any[]) => any) | undefined>(props.onBar)
expectType<string>(props.aP1) expectType<string>(props.aP1)
expectType<boolean | undefined>(props.aP2) expectType<boolean | undefined>(props.aP2)
expectType<any>(props.bP1) expectType<any>(props.bP1)
@ -561,6 +566,9 @@ describe('with mixins', () => {
render() { render() {
const props = this.$props const props = this.$props
// props // props
expectType<((...args: any[]) => any) | undefined>(props.onClick)
// from Base
expectType<((...args: any[]) => any) | undefined>(props.onBar)
expectType<string>(props.aP1) expectType<string>(props.aP1)
expectType<boolean | undefined>(props.aP2) expectType<boolean | undefined>(props.aP2)
expectType<any>(props.bP1) expectType<any>(props.bP1)
@ -688,6 +696,7 @@ describe('with extends', () => {
describe('extends with mixins', () => { describe('extends with mixins', () => {
const Mixin = defineComponent({ const Mixin = defineComponent({
emits: ['bar'],
props: { props: {
mP1: { mP1: {
type: String, type: String,
@ -706,6 +715,7 @@ describe('extends with mixins', () => {
} }
}) })
const Base = defineComponent({ const Base = defineComponent({
emits: ['foo'],
props: { props: {
p1: Boolean, p1: Boolean,
p2: { p2: {
@ -731,6 +741,7 @@ describe('extends with mixins', () => {
const MyComponent = defineComponent({ const MyComponent = defineComponent({
extends: Base, extends: Base,
mixins: [Mixin], mixins: [Mixin],
emits: ['click'],
props: { props: {
// required should make property non-void // required should make property non-void
z: { z: {
@ -741,6 +752,11 @@ describe('extends with mixins', () => {
render() { render() {
const props = this.$props const props = this.$props
// 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<boolean | undefined>(props.p1) expectType<boolean | undefined>(props.p1)
expectType<number>(props.p2) expectType<number>(props.p2)
expectType<string>(props.z) expectType<string>(props.z)
@ -879,6 +895,8 @@ describe('emits', () => {
input: (b: string) => b.length > 1 input: (b: string) => b.length > 1
}, },
setup(props, { emit }) { setup(props, { emit }) {
expectType<((n: number) => boolean) | undefined>(props.onClick)
expectType<((b: string) => boolean) | undefined>(props.onInput)
emit('click', 1) emit('click', 1)
emit('input', 'foo') emit('input', 'foo')
// @ts-expect-error // @ts-expect-error
@ -931,6 +949,8 @@ describe('emits', () => {
defineComponent({ defineComponent({
emits: ['foo', 'bar'], emits: ['foo', 'bar'],
setup(props, { emit }) { setup(props, { emit }) {
expectType<((...args: any[]) => any) | undefined>(props.onFoo)
expectType<((...args: any[]) => any) | undefined>(props.onBar)
emit('foo') emit('foo')
emit('foo', 123) emit('foo', 123)
emit('bar') emit('bar')
@ -972,10 +992,9 @@ describe('emits', () => {
}) })
describe('componentOptions setup should be `SetupContext`', () => { describe('componentOptions setup should be `SetupContext`', () => {
expect<ComponentOptions['setup']>({} as ( expect<ComponentOptions['setup']>(
props: Record<string, any>, {} as (props: Record<string, any>, ctx: SetupContext) => any
ctx: SetupContext )
) => any)
}) })
describe('extract instance type', () => { describe('extract instance type', () => {

View File

@ -5630,10 +5630,10 @@ prelude-ls@~1.1.2:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
prettier@~1.14.0: prettier@^2.3.1:
version "1.14.3" version "2.3.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.3.tgz#90238dd4c0684b7edce5f83b0fb7328e48bd0895" resolved "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6"
integrity sha512-qZDVnCrnpsRJJq5nSsiHCE3BYMED2OtsI+cmzIzF1QIfqm5ALf8tEJcO27zV1gKNKRPdhjO0dNWnrzssDQ1tFg== integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==
pretty-format@^26.0.0, pretty-format@^26.6.2: pretty-format@^26.0.0, pretty-format@^26.6.2:
version "26.6.2" version "26.6.2"