Skip to content

Commit 931bef8

Browse files
committed
feat(types): defineComponent() with generics support
BREAKING CHANGE: The type of `defineComponent()` when passing in a function has changed. This overload signature was rarely used in the past, so it is changed to something more useful. Previously the return type was `DefineComponent`, now it is a function type that inherits the generics of the function passed in. The function passed in as the first argument now also requires a return type of a render function, as the signature is no longer meant to be used for other use cases.
1 parent 28e30c8 commit 931bef8

File tree

4 files changed

+168
-28
lines changed

4 files changed

+168
-28
lines changed

packages/dts-test/defineComponent.test-d.tsx

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ describe('type inference w/ optional props declaration', () => {
351351
})
352352

353353
describe('type inference w/ direct setup function', () => {
354-
const MyComponent = defineComponent((_props: { msg: string }) => {})
354+
const MyComponent = defineComponent((_props: { msg: string }) => () => {})
355355
expectType<JSX.Element>(<MyComponent msg="foo" />)
356356
// @ts-expect-error
357357
;<MyComponent />
@@ -1250,10 +1250,121 @@ describe('prop starting with `on*` is broken', () => {
12501250
})
12511251
})
12521252

1253+
describe('function syntax w/ generics', () => {
1254+
const Comp = defineComponent(
1255+
// TODO: babel plugin to auto infer runtime props options from type
1256+
// similar to defineProps<{...}>()
1257+
<T extends string | number>(props: { msg: T; list: T[] }) => {
1258+
// use Composition API here like in <script setup>
1259+
const count = ref(0)
1260+
1261+
return () => (
1262+
// return a render function (both JSX and h() works)
1263+
<div>
1264+
{props.msg} {count.value}
1265+
</div>
1266+
)
1267+
}
1268+
)
1269+
1270+
expectType<JSX.Element>(<Comp msg="fse" list={['foo']} />)
1271+
expectType<JSX.Element>(<Comp msg={123} list={[123]} />)
1272+
1273+
expectType<JSX.Element>(
1274+
// @ts-expect-error missing prop
1275+
<Comp msg={123} />
1276+
)
1277+
1278+
expectType<JSX.Element>(
1279+
// @ts-expect-error generics don't match
1280+
<Comp msg="fse" list={[123]} />
1281+
)
1282+
expectType<JSX.Element>(
1283+
// @ts-expect-error generics don't match
1284+
<Comp msg={123} list={['123']} />
1285+
)
1286+
})
1287+
1288+
describe('function syntax w/ emits', () => {
1289+
const Foo = defineComponent(
1290+
(props: { msg: string }, ctx) => {
1291+
ctx.emit('foo')
1292+
// @ts-expect-error
1293+
ctx.emit('bar')
1294+
return () => {}
1295+
},
1296+
{
1297+
emits: ['foo']
1298+
}
1299+
)
1300+
expectType<JSX.Element>(<Foo msg="hi" onFoo={() => {}} />)
1301+
// @ts-expect-error
1302+
expectType<JSX.Element>(<Foo msg="hi" onBar={() => {}} />)
1303+
})
1304+
1305+
describe('function syntax w/ runtime props', () => {
1306+
// with runtime props, the runtime props must match
1307+
// manual type declaration
1308+
defineComponent(
1309+
(_props: { msg: string }) => {
1310+
return () => {}
1311+
},
1312+
{
1313+
props: ['msg']
1314+
}
1315+
)
1316+
1317+
defineComponent(
1318+
(_props: { msg: string }) => {
1319+
return () => {}
1320+
},
1321+
{
1322+
props: {
1323+
msg: String
1324+
}
1325+
}
1326+
)
1327+
1328+
// @ts-expect-error string prop names don't match
1329+
defineComponent(
1330+
(_props: { msg: string }) => {
1331+
return () => {}
1332+
},
1333+
{
1334+
props: ['bar']
1335+
}
1336+
)
1337+
1338+
// @ts-expect-error prop type mismatch
1339+
defineComponent(
1340+
(_props: { msg: string }) => {
1341+
return () => {}
1342+
},
1343+
{
1344+
props: {
1345+
msg: Number
1346+
}
1347+
}
1348+
)
1349+
1350+
// @ts-expect-error prop keys don't match
1351+
defineComponent(
1352+
(_props: { msg: string }, ctx) => {
1353+
return () => {}
1354+
},
1355+
{
1356+
props: {
1357+
msg: String,
1358+
bar: String
1359+
}
1360+
}
1361+
)
1362+
})
1363+
12531364
// check if defineComponent can be exported
12541365
export default {
12551366
// function components
1256-
a: defineComponent(_ => h('div')),
1367+
a: defineComponent(_ => () => h('div')),
12571368
// no props
12581369
b: defineComponent({
12591370
data() {

packages/dts-test/h.test-d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ describe('h support for generic component type', () => {
157157
describe('describeComponent extends Component', () => {
158158
// functional
159159
expectAssignable<Component>(
160-
defineComponent((_props: { foo?: string; bar: number }) => {})
160+
defineComponent((_props: { foo?: string; bar: number }) => () => {})
161161
)
162162

163163
// typed props

packages/runtime-core/__tests__/apiOptions.spec.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ describe('api: options', () => {
122122
expect(serializeInner(root)).toBe(`<div>4</div>`)
123123
})
124124

125-
test('components own methods have higher priority than global properties', async () => {
125+
test("component's own methods have higher priority than global properties", async () => {
126126
const app = createApp({
127127
methods: {
128128
foo() {
@@ -667,7 +667,7 @@ describe('api: options', () => {
667667

668668
test('mixins', () => {
669669
const calls: string[] = []
670-
const mixinA = {
670+
const mixinA = defineComponent({
671671
data() {
672672
return {
673673
a: 1
@@ -682,8 +682,8 @@ describe('api: options', () => {
682682
mounted() {
683683
calls.push('mixinA mounted')
684684
}
685-
}
686-
const mixinB = {
685+
})
686+
const mixinB = defineComponent({
687687
props: {
688688
bP: {
689689
type: String
@@ -705,7 +705,7 @@ describe('api: options', () => {
705705
mounted() {
706706
calls.push('mixinB mounted')
707707
}
708-
}
708+
})
709709
const mixinC = defineComponent({
710710
props: ['cP1', 'cP2'],
711711
data() {
@@ -727,7 +727,7 @@ describe('api: options', () => {
727727
props: {
728728
aaa: String
729729
},
730-
mixins: [defineComponent(mixinA), defineComponent(mixinB), mixinC],
730+
mixins: [mixinA, mixinB, mixinC],
731731
data() {
732732
return {
733733
c: 4,
@@ -863,7 +863,7 @@ describe('api: options', () => {
863863

864864
test('extends', () => {
865865
const calls: string[] = []
866-
const Base = {
866+
const Base = defineComponent({
867867
data() {
868868
return {
869869
a: 1,
@@ -878,9 +878,9 @@ describe('api: options', () => {
878878
expect(this.b).toBe(2)
879879
calls.push('base')
880880
}
881-
}
881+
})
882882
const Comp = defineComponent({
883-
extends: defineComponent(Base),
883+
extends: Base,
884884
data() {
885885
return {
886886
b: 2
@@ -900,7 +900,7 @@ describe('api: options', () => {
900900

901901
test('extends with mixins', () => {
902902
const calls: string[] = []
903-
const Base = {
903+
const Base = defineComponent({
904904
data() {
905905
return {
906906
a: 1,
@@ -916,8 +916,8 @@ describe('api: options', () => {
916916
expect(this.c).toBe(2)
917917
calls.push('base')
918918
}
919-
}
920-
const Mixin = {
919+
})
920+
const Mixin = defineComponent({
921921
data() {
922922
return {
923923
b: true,
@@ -930,10 +930,10 @@ describe('api: options', () => {
930930
expect(this.c).toBe(2)
931931
calls.push('mixin')
932932
}
933-
}
933+
})
934934
const Comp = defineComponent({
935-
extends: defineComponent(Base),
936-
mixins: [defineComponent(Mixin)],
935+
extends: Base,
936+
mixins: [Mixin],
937937
data() {
938938
return {
939939
c: 2

packages/runtime-core/src/apiDefineComponent.ts

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
ComponentOptionsMixin,
88
RenderFunction,
99
ComponentOptionsBase,
10-
ComponentInjectOptions
10+
ComponentInjectOptions,
11+
ComponentOptions
1112
} from './componentOptions'
1213
import {
1314
SetupContext,
@@ -17,10 +18,11 @@ import {
1718
import {
1819
ExtractPropTypes,
1920
ComponentPropsOptions,
20-
ExtractDefaultPropTypes
21+
ExtractDefaultPropTypes,
22+
ComponentObjectPropsOptions
2123
} from './componentProps'
2224
import { EmitsOptions, EmitsToProps } from './componentEmits'
23-
import { isFunction } from '@vue/shared'
25+
import { extend, isFunction } from '@vue/shared'
2426
import { VNodeProps } from './vnode'
2527
import {
2628
CreateComponentPublicInstance,
@@ -86,12 +88,34 @@ export type DefineComponent<
8688

8789
// overload 1: direct setup function
8890
// (uses user defined props interface)
89-
export function defineComponent<Props, RawBindings = object>(
91+
export function defineComponent<
92+
Props extends Record<string, any>,
93+
E extends EmitsOptions = {},
94+
EE extends string = string
95+
>(
96+
setup: (
97+
props: Props,
98+
ctx: SetupContext<E>
99+
) => RenderFunction | Promise<RenderFunction>,
100+
options?: Pick<ComponentOptions, 'name' | 'inheritAttrs'> & {
101+
props?: (keyof Props)[]
102+
emits?: E | EE[]
103+
}
104+
): (props: Props & EmitsToProps<E>) => any
105+
export function defineComponent<
106+
Props extends Record<string, any>,
107+
E extends EmitsOptions = {},
108+
EE extends string = string
109+
>(
90110
setup: (
91-
props: Readonly<Props>,
92-
ctx: SetupContext
93-
) => RawBindings | RenderFunction
94-
): DefineComponent<Props, RawBindings>
111+
props: Props,
112+
ctx: SetupContext<E>
113+
) => RenderFunction | Promise<RenderFunction>,
114+
options?: Pick<ComponentOptions, 'name' | 'inheritAttrs'> & {
115+
props?: ComponentObjectPropsOptions<Props>
116+
emits?: E | EE[]
117+
}
118+
): (props: Props & EmitsToProps<E>) => any
95119

96120
// overload 2: object format with no props
97121
// (uses user defined props interface)
@@ -198,6 +222,11 @@ export function defineComponent<
198222
): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>
199223

200224
// implementation, close to no-op
201-
export function defineComponent(options: unknown) {
202-
return isFunction(options) ? { setup: options, name: options.name } : options
225+
export function defineComponent(
226+
options: unknown,
227+
extraOptions?: ComponentOptions
228+
) {
229+
return isFunction(options)
230+
? extend({}, extraOptions, { setup: options, name: options.name })
231+
: options
203232
}

0 commit comments

Comments
 (0)