diff --git a/src/component/componentOptions.ts b/src/component/componentOptions.ts index 66116197..5a2bce77 100644 --- a/src/component/componentOptions.ts +++ b/src/component/componentOptions.ts @@ -1,11 +1,7 @@ import { Data } from './common' import { ComponentPropsOptions, ExtractPropTypes } from './componentProps' import { VNode } from 'vue' -import { - ComponentInstance, - VueProxy, - ComponentRenderProxy, -} from './componentProxy' +import { ComponentInstance, ComponentRenderProxy } from './componentProxy' import { ComponentOptions as Vue2ComponentOptions } from 'vue' @@ -52,7 +48,7 @@ interface ComponentOptionsBase< Vue2ComponentOptions, 'data' | 'computed' | 'method' | 'setup' | 'props' > { - data?: D | (() => D) + data?: (this: Props, vm: Props) => D computed?: C methods?: M } diff --git a/src/component/componentProxy.ts b/src/component/componentProxy.ts index 2fbe5360..81c3dc9d 100644 --- a/src/component/componentProxy.ts +++ b/src/component/componentProxy.ts @@ -25,18 +25,19 @@ export type ComponentRenderProxy< PublicProps = P > = { $data: D - $props: P & PublicProps + $props: Readonly

$attrs: Data $refs: Data $slots: Data $root: ComponentInstance | null $parent: ComponentInstance | null $emit: (event: string, ...args: unknown[]) => void -} & P & +} & Readonly

& UnwrapRef & D & M & - ExtractComputedReturns + ExtractComputedReturns & + Vue // for Vetur and TSX support type VueConstructorProxy = VueConstructor & { @@ -55,8 +56,8 @@ export type VueProxy< PropsOptions, RawBindings, Data = DefaultData, - Methods = DefaultMethods, - Computed = DefaultComputed + Computed = DefaultComputed, + Methods = DefaultMethods > = Vue2ComponentOptions< Vue, UnwrapRef & Data, diff --git a/src/component/defineComponent.ts b/src/component/defineComponent.ts index 4b593c74..6a904f22 100644 --- a/src/component/defineComponent.ts +++ b/src/component/defineComponent.ts @@ -8,6 +8,7 @@ import { } from './componentOptions' import { VueProxy } from './componentProxy' import { Data } from './common' +import { HasDefined } from '../types/basic' // overload 1: object format with no props export function defineComponent< @@ -43,7 +44,9 @@ export function defineComponent< M extends MethodOptions = {}, PropsOptions extends ComponentPropsOptions = ComponentPropsOptions >( - options: ComponentOptionsWithProps + options: HasDefined extends true + ? ComponentOptionsWithProps + : ComponentOptionsWithProps ): VueProxy // implementation, close to no-op export function defineComponent(options: any) { diff --git a/test-dts/defineComponent.test-d.ts b/test-dts/defineComponent.test-d.ts new file mode 100644 index 00000000..8a14f634 --- /dev/null +++ b/test-dts/defineComponent.test-d.ts @@ -0,0 +1,615 @@ +import { + ref, + reactive, + expectType, + expectError, + defineComponent, + PropType, +} from './index' + +describe('with object props', () => { + interface ExpectedProps { + a?: number | undefined + b: string + e?: Function + bb: string + cc?: string[] | undefined + dd: { n: 1 } + ee?: () => string + ff?: (a: number, b: string) => { a: boolean } + ccc?: string[] | undefined + ddd: string[] + eee: () => { a: string } + fff: (a: number, b: string) => { a: boolean } + hhh: boolean + } + + type GT = string & { __brand: unknown } + + defineComponent({ + props: { + a: Number, + // required should make property non-void + b: { + type: String, + required: true, + }, + e: Function, + // default value should infer type and make it non-void + bb: { + default: 'hello', + }, + // explicit type casting + cc: Array as PropType, + // required + type casting + dd: { + type: Object as PropType<{ n: 1 }>, + required: true, + }, + // return type + ee: Function as PropType<() => string>, + // arguments + object return + ff: Function as PropType<(a: number, b: string) => { a: boolean }>, + // explicit type casting with constructor + ccc: Array as () => string[], + // required + contructor type casting + ddd: { + type: Array as () => string[], + required: true, + }, + // required + object return + eee: { + type: Function as PropType<() => { a: string }>, + required: true, + }, + // required + arguments + object return + fff: { + type: Function as PropType<(a: number, b: string) => { a: boolean }>, + required: true, + }, + hhh: { + type: Boolean, + required: true, + }, + }, + setup(props) { + // type assertion. See https://github.com/SamVerschueren/tsd + expectType(props.a) + expectType(props.b) + expectType(props.e) + expectType(props.bb) + expectType(props.cc) + expectType(props.dd) + expectType(props.ee) + expectType(props.ff) + expectType(props.ccc) + expectType(props.ddd) + expectType(props.eee) + expectType(props.fff) + expectType(props.hhh) + + expectError((props.a = 1)) + + // setup context + return { + c: ref(1), + d: { + e: ref('hi'), + }, + f: reactive({ + g: ref('hello' as GT), + }), + } + }, + render(h) { + const props = this.$props + expectType(props.a) + expectType(props.b) + expectType(props.e) + expectType(props.bb) + expectType(props.cc) + expectType(props.dd) + expectType(props.ee) + expectType(props.ff) + expectType(props.ccc) + expectType(props.ddd) + expectType(props.eee) + expectType(props.fff) + expectType(props.hhh) + + // @ts-expect-error props should be readonly + expectError((props.a = 1)) + + // should also expose declared props on `this` + expectType(this.a) + expectType(this.b) + expectType(this.e) + expectType(this.bb) + expectType(this.cc) + expectType(this.dd) + expectType(this.ee) + expectType(this.ff) + expectType(this.ccc) + expectType(this.ddd) + expectType(this.eee) + expectType(this.fff) + expectType(this.hhh) + + // @ts-expect-error props on `this` should be readonly + expectError((this.a = 1)) + + // assert setup context unwrapping + expectType(this.c) + expectType(this.d.e) + expectType(this.f.g) + + // setup context properties should be mutable + this.c = 2 + + return h() + }, + }) +}) + +describe('type inference w/ array props declaration', () => { + defineComponent({ + props: ['a', 'b'], + setup(props) { + // @ts-expect-error props should be readonly + expectError((props.a = 1)) + expectType(props.a) + expectType(props.b) + return { + c: 1, + } + }, + render(h) { + expectType(this.$props.a) + expectType(this.$props.b) + // @ts-expect-error + expectError((this.$props.a = 1)) + expectType(this.a) + expectType(this.b) + expectType(this.c) + + return h() + }, + }) +}) + +describe('type inference w/ options API', () => { + defineComponent({ + props: { a: Number }, + setup() { + return { + b: 123, + } + }, + data() { + // Limitation: we cannot expose the return result of setup() on `this` + // here in data() - somehow that would mess up the inference + expectType(this.a) + return { + c: this.a || 123, + } + }, + computed: { + d(): number { + expectType(this.b) + return this.b + 1 + }, + }, + watch: { + a() { + expectType(this.b) + this.b + 1 + }, + }, + created() { + // props + expectType(this.a) + // returned from setup() + expectType(this.b) + // returned from data() + expectType(this.c) + // computed + expectType(this.d) + }, + methods: { + doSomething() { + // props + expectType(this.a) + // returned from setup() + expectType(this.b) + // returned from data() + expectType(this.c) + // computed + expectType(this.d) + }, + }, + render(h) { + // props + expectType(this.a) + // returned from setup() + expectType(this.b) + // returned from data() + expectType(this.c) + // computed + expectType(this.d) + + return h() + }, + }) +}) +describe('with mixins', () => { + /* + const MixinA = defineComponent({ + props: { + aP1: { + type: String, + default: 'aP1' + }, + aP2: Boolean + }, + data() { + return { + a: 1 + } + } + }) + const MixinB = defineComponent({ + props: ['bP1', 'bP2'], + data() { + return { + b: 2 + } + } + }) + const MixinC = defineComponent({ + data() { + return { + c: 3 + } + } + }) + const MixinD = defineComponent({ + mixins: [MixinA], + data() { + return { + d: 4 + } + }, + computed: { + dC1(): number { + return this.d + this.a + }, + dC2(): string { + return this.aP1 + 'dC2' + } + } + }) + const MyComponent = defineComponent({ + mixins: [MixinA, MixinB, MixinC, MixinD], + props: { + // required should make property non-void + z: { + type: String, + required: true + } + }, + render() { + const props = this.$props + // props + expectType(props.aP1) + expectType(props.aP2) + expectType(props.bP1) + expectType(props.bP2) + expectType(props.z) + + const data = this.$data + expectType(data.a) + expectType(data.b) + expectType(data.c) + expectType(data.d) + + // should also expose declared props on `this` + expectType(this.a) + expectType(this.aP1) + expectType(this.aP2) + expectType(this.b) + expectType(this.bP1) + expectType(this.c) + expectType(this.d) + expectType(this.dC1) + expectType(this.dC2) + + // props should be readonly + // @ts-expect-error + expectError((this.aP1 = 'new')) + // @ts-expect-error + expectError((this.z = 1)) + + // props on `this` should be readonly + // @ts-expect-error + expectError((this.bP1 = 1)) + + // string value can not assigned to number type value + // @ts-expect-error + expectError((this.c = '1')) + + // setup context properties should be mutable + this.d = 5 + + return null + } + }) + + // Test TSX + expectType( + + ) + + // missing required props + // @ts-expect-error + expectError() + + // wrong prop types + // @ts-expect-error + expectError() + // @ts-expect-error + expectError() + */ +}) + +describe('with extends', () => { + /* + const Base = defineComponent({ + props: { + aP1: Boolean, + aP2: { + type: Number, + default: 2 + } + }, + data() { + return { + a: 1 + } + }, + computed: { + c(): number { + return this.aP2 + this.a + } + } + }) + const MyComponent = defineComponent({ + extends: Base, + props: { + // required should make property non-void + z: { + type: String, + required: true + } + }, + render() { + const props = this.$props + // props + expectType(props.aP1) + expectType(props.aP2) + expectType(props.z) + + const data = this.$data + expectType(data.a) + + // should also expose declared props on `this` + expectType(this.a) + expectType(this.aP1) + expectType(this.aP2) + + // setup context properties should be mutable + this.a = 5 + + return null + } + }) + + // Test TSX + expectType() + + // missing required props + // @ts-expect-error + expectError() + + // wrong prop types + // @ts-expect-error + expectError() + // @ts-expect-error + expectError() + */ +}) +describe('extends with mixins', () => { + /* + const Mixin = defineComponent({ + props: { + mP1: { + type: String, + default: 'mP1' + }, + mP2: Boolean + }, + data() { + return { + a: 1 + } + } + }) + const Base = defineComponent({ + props: { + p1: Boolean, + p2: { + type: Number, + default: 2 + } + }, + data() { + return { + b: 2 + } + }, + computed: { + c(): number { + return this.p2 + this.b + } + } + }) + const MyComponent = defineComponent({ + extends: Base, + mixins: [Mixin], + props: { + // required should make property non-void + z: { + type: String, + required: true + } + }, + render() { + const props = this.$props + // props + expectType(props.p1) + expectType(props.p2) + expectType(props.z) + expectType(props.mP1) + expectType(props.mP2) + + const data = this.$data + expectType(data.a) + expectType(data.b) + + // should also expose declared props on `this` + expectType(this.a) + expectType(this.b) + expectType(this.p1) + expectType(this.p2) + expectType(this.mP1) + expectType(this.mP2) + + // setup context properties should be mutable + this.a = 5 + + return null + } + }) + + // Test TSX + expectType() + + // missing required props + // @ts-expect-error + expectError() + + // wrong prop types + // @ts-expect-error + expectError() + // @ts-expect-error + expectError() + */ +}) + +describe('compatibility w/ createApp', () => { + /* + const comp = defineComponent({}) + createApp(comp).mount('#hello') + + const comp2 = defineComponent({ + props: { foo: String } + }) + createApp(comp2).mount('#hello') + + const comp3 = defineComponent({ + setup() { + return { + a: 1 + } + } + }) + createApp(comp3).mount('#hello') + */ +}) + +describe('defineComponent', () => { + test('should accept components defined with defineComponent', () => { + const comp = defineComponent({}) + defineComponent({ + components: { comp }, + }) + }) +}) + +describe('emits', () => { + /* + + // Note: for TSX inference, ideally we want to map emits to onXXX props, + // but that requires type-level string constant concatenation as suggested in + // https://github.com/Microsoft/TypeScript/issues/12754 + + // The workaround for TSX users is instead of using emits, declare onXXX props + // and call them instead. Since `v-on:click` compiles to an `onClick` prop, + // this would also support other users consuming the component in templates + // with `v-on` listeners. + + // with object emits + defineComponent({ + emits: { + click: (n: number) => typeof n === 'number', + input: (b: string) => null + }, + setup(props, { emit }) { + emit('click', 1) + emit('input', 'foo') + // @ts-expect-error + expectError(emit('nope')) + // @ts-expect-error + expectError(emit('click')) + // @ts-expect-error + expectError(emit('click', 'foo')) + // @ts-expect-error + expectError(emit('input')) + // @ts-expect-error + expectError(emit('input', 1)) + }, + created() { + this.$emit('click', 1) + this.$emit('input', 'foo') + // @ts-expect-error + expectError(this.$emit('nope')) + // @ts-expect-error + expectError(this.$emit('click')) + // @ts-expect-error + expectError(this.$emit('click', 'foo')) + // @ts-expect-error + expectError(this.$emit('input')) + // @ts-expect-error + expectError(this.$emit('input', 1)) + } + }) + + // with array emits + defineComponent({ + emits: ['foo', 'bar'], + setup(props, { emit }) { + emit('foo') + emit('foo', 123) + emit('bar') + // @ts-expect-error + expectError(emit('nope')) + }, + created() { + this.$emit('foo') + this.$emit('foo', 123) + this.$emit('bar') + // @ts-expect-error + expectError(this.$emit('nope')) + } + }) + */ +}) diff --git a/test-dts/index.d.ts b/test-dts/index.d.ts index bd365356..551b2ba5 100644 --- a/test-dts/index.d.ts +++ b/test-dts/index.d.ts @@ -1,5 +1,7 @@ export * from '@vue/composition-api' +export function describe(_name: string, _fn: () => void): void + export function expectType(value: T): void export function expectError(value: T): void export function expectAssignable(value: T2): void