Skip to content

Commit 69344ff

Browse files
authored
feat(types): map declared emits to onXXX props in inferred prop types (#3926)
1 parent 35cc7b0 commit 69344ff

File tree

7 files changed

+93
-51
lines changed

7 files changed

+93
-51
lines changed

.prettierrc

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
semi: false
22
singleQuote: true
33
printWidth: 80
4+
trailingComma: 'none'
5+
arrowParens: 'avoid'

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
"lint-staged": "^10.2.10",
6969
"minimist": "^1.2.0",
7070
"npm-run-all": "^4.1.5",
71-
"prettier": "~1.14.0",
71+
"prettier": "^2.3.1",
7272
"puppeteer": "^10.0.0",
7373
"rollup": "~2.38.5",
7474
"rollup-plugin-node-builtins": "^2.1.2",

packages/runtime-core/src/apiDefineComponent.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
ComponentPropsOptions,
1919
ExtractDefaultPropTypes
2020
} from './componentProps'
21-
import { EmitsOptions } from './componentEmits'
21+
import { EmitsOptions, EmitsToProps } from './componentEmits'
2222
import { isFunction } from '@vue/shared'
2323
import { VNodeProps } from './vnode'
2424
import {
@@ -41,7 +41,7 @@ export type DefineComponent<
4141
E extends EmitsOptions = Record<string, any>,
4242
EE extends string = string,
4343
PP = PublicProps,
44-
Props = Readonly<ExtractPropTypes<PropsOrPropOptions>>,
44+
Props = Readonly<ExtractPropTypes<PropsOrPropOptions>> & EmitsToProps<E>,
4545
Defaults = ExtractDefaultPropTypes<PropsOrPropOptions>
4646
> = ComponentPublicInstanceConstructor<
4747
CreateComponentPublicInstance<
@@ -102,7 +102,7 @@ export function defineComponent<
102102
EE extends string = string
103103
>(
104104
options: ComponentOptionsWithoutProps<
105-
Props,
105+
Props & EmitsToProps<E>,
106106
RawBindings,
107107
D,
108108
C,

packages/runtime-core/src/componentEmits.ts

+24-8
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,38 @@ export type ObjectEmitsOptions = Record<
3131
string,
3232
((...args: any[]) => any) | null
3333
>
34+
3435
export type EmitsOptions = ObjectEmitsOptions | string[]
3536

37+
export type EmitsToProps<T extends EmitsOptions> = T extends string[]
38+
? {
39+
[K in string & `on${Capitalize<T[number]>}`]?: (...args: any[]) => any
40+
}
41+
: T extends ObjectEmitsOptions
42+
? {
43+
[K in string &
44+
`on${Capitalize<string & keyof T>}`]?: K extends `on${infer C}`
45+
? T[Uncapitalize<C>] extends null
46+
? (...args: any[]) => any
47+
: T[Uncapitalize<C>]
48+
: never
49+
}
50+
: {}
51+
3652
export type EmitFn<
3753
Options = ObjectEmitsOptions,
3854
Event extends keyof Options = keyof Options
3955
> = Options extends Array<infer V>
4056
? (event: V, ...args: any[]) => void
4157
: {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function
42-
? (event: string, ...args: any[]) => void
43-
: UnionToIntersection<
44-
{
45-
[key in Event]: Options[key] extends ((...args: infer Args) => any)
46-
? (event: key, ...args: Args) => void
47-
: (event: key, ...args: any[]) => void
48-
}[Event]
49-
>
58+
? (event: string, ...args: any[]) => void
59+
: UnionToIntersection<
60+
{
61+
[key in Event]: Options[key] extends (...args: infer Args) => any
62+
? (event: key, ...args: Args) => void
63+
: (event: key, ...args: any[]) => void
64+
}[Event]
65+
>
5066

5167
export function emit(
5268
instance: ComponentInternalInstance,

packages/runtime-core/src/componentOptions.ts

+36-31
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import {
5151
ExtractPropTypes,
5252
ExtractDefaultPropTypes
5353
} from './componentProps'
54-
import { EmitsOptions } from './componentEmits'
54+
import { EmitsOptions, EmitsToProps } from './componentEmits'
5555
import { Directive } from './directives'
5656
import {
5757
CreateComponentPublicInstance,
@@ -91,16 +91,18 @@ export interface ComponentCustomOptions {}
9191
export type RenderFunction = () => VNodeChild
9292

9393
type ExtractOptionProp<T> = T extends ComponentOptionsBase<
94-
infer P,
95-
any,
96-
any,
97-
any,
98-
any,
99-
any,
100-
any,
101-
any
94+
infer P, // Props
95+
any, // RawBindings
96+
any, // D
97+
any, // C
98+
any, // M
99+
any, // Mixin
100+
any, // Extends
101+
any // EmitsOptions
102102
>
103-
? unknown extends P ? {} : P
103+
? unknown extends P
104+
? {}
105+
: P
104106
: {}
105107

106108
export interface ComponentOptionsBase<
@@ -114,8 +116,7 @@ export interface ComponentOptionsBase<
114116
E extends EmitsOptions,
115117
EE extends string = string,
116118
Defaults = {}
117-
>
118-
extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
119+
> extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
119120
ComponentInternalOptions,
120121
ComponentCustomOptions {
121122
setup?: (
@@ -220,9 +221,10 @@ export type ComponentOptionsWithoutProps<
220221
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
221222
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
222223
E extends EmitsOptions = EmitsOptions,
223-
EE extends string = string
224+
EE extends string = string,
225+
PE = Props & EmitsToProps<E>
224226
> = ComponentOptionsBase<
225-
Props,
227+
PE,
226228
RawBindings,
227229
D,
228230
C,
@@ -235,7 +237,7 @@ export type ComponentOptionsWithoutProps<
235237
> & {
236238
props?: undefined
237239
} & ThisType<
238-
CreateComponentPublicInstance<{}, RawBindings, D, C, M, Mixin, Extends, E>
240+
CreateComponentPublicInstance<PE, RawBindings, D, C, M, Mixin, Extends, E>
239241
>
240242

241243
export type ComponentOptionsWithArrayProps<
@@ -248,7 +250,7 @@ export type ComponentOptionsWithArrayProps<
248250
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
249251
E extends EmitsOptions = EmitsOptions,
250252
EE extends string = string,
251-
Props = Readonly<{ [key in PropNames]?: any }>
253+
Props = Readonly<{ [key in PropNames]?: any }> & EmitsToProps<E>
252254
> = ComponentOptionsBase<
253255
Props,
254256
RawBindings,
@@ -285,7 +287,7 @@ export type ComponentOptionsWithObjectProps<
285287
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
286288
E extends EmitsOptions = EmitsOptions,
287289
EE extends string = string,
288-
Props = Readonly<ExtractPropTypes<PropsOptions>>,
290+
Props = Readonly<ExtractPropTypes<PropsOptions>> & EmitsToProps<E>,
289291
Defaults = ExtractDefaultPropTypes<PropsOptions>
290292
> = ComponentOptionsBase<
291293
Props,
@@ -365,7 +367,9 @@ export interface MethodOptions {
365367
export type ExtractComputedReturns<T extends any> = {
366368
[key in keyof T]: T[key] extends { get: (...args: any[]) => infer TReturn }
367369
? TReturn
368-
: T[key] extends (...args: any[]) => infer TReturn ? TReturn : never
370+
: T[key] extends (...args: any[]) => infer TReturn
371+
? TReturn
372+
: never
369373
}
370374

371375
export type ObjectWatchOptionItem = {
@@ -471,7 +475,7 @@ interface LegacyOptions<
471475
__differentiator?: keyof D | keyof C | keyof M
472476
}
473477

474-
type MergedHook<T = (() => void)> = T | T[]
478+
type MergedHook<T = () => void> = T | T[]
475479

476480
export type MergedComponentOptions = ComponentOptions &
477481
MergedComponentOptionsOverride
@@ -679,21 +683,21 @@ export function applyOptions(instance: ComponentInternalInstance) {
679683
const get = isFunction(opt)
680684
? opt.bind(publicThis, publicThis)
681685
: isFunction(opt.get)
682-
? opt.get.bind(publicThis, publicThis)
683-
: NOOP
686+
? opt.get.bind(publicThis, publicThis)
687+
: NOOP
684688
if (__DEV__ && get === NOOP) {
685689
warn(`Computed property "${key}" has no getter.`)
686690
}
687691
const set =
688692
!isFunction(opt) && isFunction(opt.set)
689693
? opt.set.bind(publicThis)
690694
: __DEV__
691-
? () => {
692-
warn(
693-
`Write operation failed: computed property "${key}" is readonly.`
694-
)
695-
}
696-
: NOOP
695+
? () => {
696+
warn(
697+
`Write operation failed: computed property "${key}" is readonly.`
698+
)
699+
}
700+
: NOOP
697701
const c = computed({
698702
get,
699703
set
@@ -1006,10 +1010,11 @@ function mergeDataFn(to: any, from: any) {
10061010
return from
10071011
}
10081012
return function mergedDataFn(this: ComponentPublicInstance) {
1009-
return (__COMPAT__ &&
1010-
isCompatEnabled(DeprecationTypes.OPTIONS_DATA_MERGE, null)
1011-
? deepMergeData
1012-
: extend)(
1013+
return (
1014+
__COMPAT__ && isCompatEnabled(DeprecationTypes.OPTIONS_DATA_MERGE, null)
1015+
? deepMergeData
1016+
: extend
1017+
)(
10131018
isFunction(to) ? to.call(this, this) : to,
10141019
isFunction(from) ? from.call(this, this) : from
10151020
)

test-dts/defineComponent.test-d.tsx

+23-4
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@ describe('type inference w/ options API', () => {
469469

470470
describe('with mixins', () => {
471471
const MixinA = defineComponent({
472+
emits: ['bar'],
472473
props: {
473474
aP1: {
474475
type: String,
@@ -523,6 +524,7 @@ describe('with mixins', () => {
523524
})
524525
const MyComponent = defineComponent({
525526
mixins: [MixinA, MixinB, MixinC, MixinD],
527+
emits: ['click'],
526528
props: {
527529
// required should make property non-void
528530
z: {
@@ -552,6 +554,9 @@ describe('with mixins', () => {
552554
setup(props) {
553555
expectType<string>(props.z)
554556
// props
557+
expectType<((...args: any[]) => any) | undefined>(props.onClick)
558+
// from Base
559+
expectType<((...args: any[]) => any) | undefined>(props.onBar)
555560
expectType<string>(props.aP1)
556561
expectType<boolean | undefined>(props.aP2)
557562
expectType<any>(props.bP1)
@@ -561,6 +566,9 @@ describe('with mixins', () => {
561566
render() {
562567
const props = this.$props
563568
// props
569+
expectType<((...args: any[]) => any) | undefined>(props.onClick)
570+
// from Base
571+
expectType<((...args: any[]) => any) | undefined>(props.onBar)
564572
expectType<string>(props.aP1)
565573
expectType<boolean | undefined>(props.aP2)
566574
expectType<any>(props.bP1)
@@ -688,6 +696,7 @@ describe('with extends', () => {
688696

689697
describe('extends with mixins', () => {
690698
const Mixin = defineComponent({
699+
emits: ['bar'],
691700
props: {
692701
mP1: {
693702
type: String,
@@ -706,6 +715,7 @@ describe('extends with mixins', () => {
706715
}
707716
})
708717
const Base = defineComponent({
718+
emits: ['foo'],
709719
props: {
710720
p1: Boolean,
711721
p2: {
@@ -731,6 +741,7 @@ describe('extends with mixins', () => {
731741
const MyComponent = defineComponent({
732742
extends: Base,
733743
mixins: [Mixin],
744+
emits: ['click'],
734745
props: {
735746
// required should make property non-void
736747
z: {
@@ -741,6 +752,11 @@ describe('extends with mixins', () => {
741752
render() {
742753
const props = this.$props
743754
// props
755+
expectType<((...args: any[]) => any) | undefined>(props.onClick)
756+
// from Mixin
757+
expectType<((...args: any[]) => any) | undefined>(props.onBar)
758+
// from Base
759+
expectType<((...args: any[]) => any) | undefined>(props.onFoo)
744760
expectType<boolean | undefined>(props.p1)
745761
expectType<number>(props.p2)
746762
expectType<string>(props.z)
@@ -879,6 +895,8 @@ describe('emits', () => {
879895
input: (b: string) => b.length > 1
880896
},
881897
setup(props, { emit }) {
898+
expectType<((n: number) => boolean) | undefined>(props.onClick)
899+
expectType<((b: string) => boolean) | undefined>(props.onInput)
882900
emit('click', 1)
883901
emit('input', 'foo')
884902
// @ts-expect-error
@@ -931,6 +949,8 @@ describe('emits', () => {
931949
defineComponent({
932950
emits: ['foo', 'bar'],
933951
setup(props, { emit }) {
952+
expectType<((...args: any[]) => any) | undefined>(props.onFoo)
953+
expectType<((...args: any[]) => any) | undefined>(props.onBar)
934954
emit('foo')
935955
emit('foo', 123)
936956
emit('bar')
@@ -972,10 +992,9 @@ describe('emits', () => {
972992
})
973993

974994
describe('componentOptions setup should be `SetupContext`', () => {
975-
expect<ComponentOptions['setup']>({} as (
976-
props: Record<string, any>,
977-
ctx: SetupContext
978-
) => any)
995+
expect<ComponentOptions['setup']>(
996+
{} as (props: Record<string, any>, ctx: SetupContext) => any
997+
)
979998
})
980999

9811000
describe('extract instance type', () => {

yarn.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -5630,10 +5630,10 @@ prelude-ls@~1.1.2:
56305630
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
56315631
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
56325632

5633-
prettier@~1.14.0:
5634-
version "1.14.3"
5635-
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.3.tgz#90238dd4c0684b7edce5f83b0fb7328e48bd0895"
5636-
integrity sha512-qZDVnCrnpsRJJq5nSsiHCE3BYMED2OtsI+cmzIzF1QIfqm5ALf8tEJcO27zV1gKNKRPdhjO0dNWnrzssDQ1tFg==
5633+
prettier@^2.3.1:
5634+
version "2.3.1"
5635+
resolved "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6"
5636+
integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==
56375637

56385638
pretty-format@^26.0.0, pretty-format@^26.6.2:
56395639
version "26.6.2"

0 commit comments

Comments
 (0)