Skip to content

Commit d77557c

Browse files
authored
feat(types): defineComponent() with generics support (#7963)
BREAKING CHANGE: The type of `defineComponent()` when passing in a function has changed. This overload signature is rarely used in practice and the breakage will be minimal, so repurposing it to something more useful should be worth it. close #3102
1 parent 9a8073d commit d77557c

File tree

4 files changed

+193
-28
lines changed

4 files changed

+193
-28
lines changed

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

+122-2
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,130 @@ 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+
<T extends string>(_props: { msg: T }) => {
1319+
return () => {}
1320+
},
1321+
{
1322+
props: ['msg']
1323+
}
1324+
)
1325+
1326+
defineComponent(
1327+
<T extends string>(_props: { msg: T }) => {
1328+
return () => {}
1329+
},
1330+
{
1331+
props: {
1332+
msg: String
1333+
}
1334+
}
1335+
)
1336+
1337+
// @ts-expect-error string prop names don't match
1338+
defineComponent(
1339+
(_props: { msg: string }) => {
1340+
return () => {}
1341+
},
1342+
{
1343+
props: ['bar']
1344+
}
1345+
)
1346+
1347+
// @ts-expect-error prop type mismatch
1348+
defineComponent(
1349+
(_props: { msg: string }) => {
1350+
return () => {}
1351+
},
1352+
{
1353+
props: {
1354+
msg: Number
1355+
}
1356+
}
1357+
)
1358+
1359+
// @ts-expect-error prop keys don't match
1360+
defineComponent(
1361+
(_props: { msg: string }, ctx) => {
1362+
return () => {}
1363+
},
1364+
{
1365+
props: {
1366+
msg: String,
1367+
bar: String
1368+
}
1369+
}
1370+
)
1371+
})
1372+
12531373
// check if defineComponent can be exported
12541374
export default {
12551375
// function components
1256-
a: defineComponent(_ => h('div')),
1376+
a: defineComponent(_ => () => h('div')),
12571377
// no props
12581378
b: defineComponent({
12591379
data() {

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

+1-1
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

+31-15
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,
@@ -817,6 +817,22 @@ describe('api: options', () => {
817817
])
818818
})
819819

820+
test('unlikely mixin usage', () => {
821+
const MixinA = {
822+
data() {}
823+
}
824+
const MixinB = {
825+
data() {}
826+
}
827+
defineComponent({
828+
// @ts-expect-error edge case after #7963, unlikely to happen in practice
829+
// since the user will want to type the mixins themselves.
830+
mixins: [defineComponent(MixinA), defineComponent(MixinB)],
831+
// @ts-expect-error
832+
data() {}
833+
})
834+
})
835+
820836
test('chained extends in mixins', () => {
821837
const calls: string[] = []
822838

@@ -863,7 +879,7 @@ describe('api: options', () => {
863879

864880
test('extends', () => {
865881
const calls: string[] = []
866-
const Base = {
882+
const Base = defineComponent({
867883
data() {
868884
return {
869885
a: 1,
@@ -878,9 +894,9 @@ describe('api: options', () => {
878894
expect(this.b).toBe(2)
879895
calls.push('base')
880896
}
881-
}
897+
})
882898
const Comp = defineComponent({
883-
extends: defineComponent(Base),
899+
extends: Base,
884900
data() {
885901
return {
886902
b: 2
@@ -900,7 +916,7 @@ describe('api: options', () => {
900916

901917
test('extends with mixins', () => {
902918
const calls: string[] = []
903-
const Base = {
919+
const Base = defineComponent({
904920
data() {
905921
return {
906922
a: 1,
@@ -916,8 +932,8 @@ describe('api: options', () => {
916932
expect(this.c).toBe(2)
917933
calls.push('base')
918934
}
919-
}
920-
const Mixin = {
935+
})
936+
const Mixin = defineComponent({
921937
data() {
922938
return {
923939
b: true,
@@ -930,10 +946,10 @@ describe('api: options', () => {
930946
expect(this.c).toBe(2)
931947
calls.push('mixin')
932948
}
933-
}
949+
})
934950
const Comp = defineComponent({
935-
extends: defineComponent(Base),
936-
mixins: [defineComponent(Mixin)],
951+
extends: Base,
952+
mixins: [Mixin],
937953
data() {
938954
return {
939955
c: 2

packages/runtime-core/src/apiDefineComponent.ts

+39-10
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)