Skip to content

Commit 9411ec5

Browse files
committed
fix(type): fix type inference for component without props
Also remove `props` and `setupContext` from `render` in `setup()`. Resolves: #112
1 parent 3b8cfc2 commit 9411ec5

File tree

5 files changed

+118
-50
lines changed

5 files changed

+118
-50
lines changed

src/component/component.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,13 @@ export interface SetupContext {
4949
emit(event: string, ...args: any[]): void;
5050
}
5151

52-
type RenderFunction<Props> = (props: Props, ctx: SetupContext) => VNode;
53-
5452
export type SetupFunction<Props, RawBindings> = (
5553
this: void,
5654
props: Props,
5755
ctx: SetupContext
58-
) => RawBindings | RenderFunction<Props>;
56+
) => RawBindings | (() => VNode | null);
5957

60-
interface ComponentOptions<
58+
interface ComponentOptionsWithProps<
6159
PropsOptions = ComponentPropsOptions,
6260
RawBindings = Data,
6361
Props = ExtractPropTypes<PropsOptions>
@@ -66,16 +64,25 @@ interface ComponentOptions<
6664
setup?: SetupFunction<Props, RawBindings>;
6765
}
6866

69-
// object format with object props declaration
67+
interface ComponentOptionsWithoutProps<Props = never, RawBindings = Data> {
68+
props?: undefined;
69+
setup?: SetupFunction<Props, RawBindings>;
70+
}
71+
72+
// overload 1: object format with no props
73+
export function createComponent<RawBindings>(
74+
options: ComponentOptionsWithoutProps<never, RawBindings>
75+
): VueProxy<never, RawBindings>;
76+
// overload 2: object format with object props declaration
7077
// see `ExtractPropTypes` in ./componentProps.ts
7178
export function createComponent<Props, RawBindings = Data, PropsOptions = ComponentPropsOptions>(
7279
// prettier-ignore
7380
options: (
7481
// prefer the provided Props, otherwise infer it from PropsOptions
7582
HasDefined<Props> extends true
76-
? ComponentOptions<PropsOptions, RawBindings, Props>
77-
: ComponentOptions<PropsOptions, RawBindings>) &
78-
Omit<Vue2ComponentOptions<Vue>, keyof ComponentOptions<never, never>>
83+
? ComponentOptionsWithProps<PropsOptions, RawBindings, Props>
84+
: ComponentOptionsWithProps<PropsOptions, RawBindings>) &
85+
Omit<Vue2ComponentOptions<Vue>, keyof ComponentOptionsWithProps<never, never>>
7986
): VueProxy<PropsOptions, RawBindings>;
8087
// implementation, close to no-op
8188
export function createComponent(options: any) {

src/setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export function mixin(Vue: VueConstructor) {
176176
// keep currentInstance accessible for createElement
177177
vm.$options.render = () => {
178178
resolveScopedSlots(vm, ctx.slots);
179-
return activateCurrentInstance(vm, vm_ => bindingFunc(vm_.$props, ctx));
179+
return activateCurrentInstance(vm, () => bindingFunc());
180180
};
181181
return;
182182
}

test/createComponent.spec.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.

test/setup.spec.js

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -295,46 +295,37 @@ describe('setup', () => {
295295
});
296296

297297
it('inline render function should receive proper params', () => {
298-
let p, c;
298+
let p;
299299
const vm = new Vue({
300300
template: `<child msg="foo" a="1" b="2"></child>`,
301301
components: {
302302
child: {
303303
name: 'child',
304304
props: ['msg'],
305305
setup() {
306-
return (props, ctx) => {
306+
return props => {
307307
p = props;
308-
c = ctx;
309308
return null;
310309
};
311310
},
312311
},
313312
},
314313
}).$mount();
315-
expect(p).toEqual({
316-
msg: 'foo',
317-
});
318-
expect(c).toBeDefined();
319-
expect(c.root).toBe(vm);
320-
expect(c.attrs).toEqual({
321-
a: '1',
322-
b: '2',
323-
});
314+
expect(p).toBe(undefined);
324315
});
325316

326317
it('inline render function should work', done => {
327318
// let createElement;
328319
const vm = new Vue({
329320
props: ['msg'],
330321
template: '<div>1</div>',
331-
setup() {
322+
setup(props) {
332323
const count = ref(0);
333324
const increment = () => {
334325
count.value++;
335326
};
336327

337-
return props =>
328+
return () =>
338329
h('div', [
339330
h('span', props.msg),
340331
h(

test/types/createComponent.spec.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { createComponent, createElement as h, ref, SetupContext } from '../../src';
2+
const Vue = require('vue/dist/vue.common.js');
3+
4+
type Equal<Left, Right> = (<U>() => U extends Left ? 1 : 0) extends (<U>() => U extends Right
5+
? 1
6+
: 0)
7+
? true
8+
: false;
9+
10+
const isTypeEqual = <Left, Right>(shouldBeEqual: Equal<Left, Right>) => {
11+
void shouldBeEqual;
12+
expect(true).toBe(true);
13+
};
14+
const isSubType = <SuperType, SubType>(shouldBeEqual: SubType extends SuperType ? true : false) => {
15+
void shouldBeEqual;
16+
expect(true).toBe(true);
17+
};
18+
19+
describe('createComponent', () => {
20+
it('should work', () => {
21+
const Child = createComponent({
22+
props: { msg: String },
23+
setup(props) {
24+
return () => h('span', props.msg);
25+
},
26+
});
27+
28+
const App = createComponent({
29+
setup() {
30+
const msg = ref('hello');
31+
return () =>
32+
h('div', [
33+
h(Child, {
34+
props: {
35+
msg: msg.value,
36+
},
37+
}),
38+
]);
39+
},
40+
});
41+
const vm = new Vue(App).$mount();
42+
expect(vm.$el.querySelector('span').textContent).toBe('hello');
43+
});
44+
45+
it('should infer props type', () => {
46+
const App = createComponent({
47+
props: {
48+
a: {
49+
type: Number,
50+
default: 0,
51+
},
52+
b: String,
53+
},
54+
setup(props, ctx) {
55+
type PropsType = typeof props;
56+
isTypeEqual<SetupContext, typeof ctx>(true);
57+
isSubType<PropsType, { readonly b?: string; readonly a: number }>(true);
58+
isSubType<{ readonly b?: string; readonly a: number }, PropsType>(true);
59+
return () => null;
60+
},
61+
});
62+
new Vue(App).$mount();
63+
expect.assertions(3);
64+
});
65+
66+
it('custom props interface', () => {
67+
interface IPropsType {
68+
b: string;
69+
}
70+
const App = createComponent<IPropsType>({
71+
props: {
72+
b: {},
73+
},
74+
setup(props, ctx) {
75+
type PropsType = typeof props;
76+
isTypeEqual<SetupContext, typeof ctx>(true);
77+
isSubType<PropsType, { b: string }>(true);
78+
isSubType<{ b: string }, PropsType>(true);
79+
return () => null;
80+
},
81+
});
82+
new Vue(App).$mount();
83+
expect.assertions(3);
84+
});
85+
86+
it('no props', () => {
87+
const App = createComponent({
88+
setup(props, ctx) {
89+
isTypeEqual<SetupContext, typeof ctx>(true);
90+
isTypeEqual<never, typeof props>(true);
91+
return () => null;
92+
},
93+
});
94+
new Vue(App).$mount();
95+
expect.assertions(2);
96+
});
97+
});

0 commit comments

Comments
 (0)