import { describe, defineComponent, PropType, ref, reactive, createApp, expectError, expectType, ComponentPublicInstance, ComponentOptions, SetupContext } 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 } const MyComponent = 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) // @ts-expect-error props should be readonly expectError((props.a = 1)) // setup context return { c: ref(1), d: { e: ref('hi') }, f: reactive({ g: ref('hello' as GT) }) } }, render() { 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 null } }) // Test TSX expectType( {}} cc={['cc']} dd={{ n: 1 }} ee={() => 'ee'} ccc={['ccc']} ddd={['ddd']} eee={() => ({ a: 'eee' })} fff={(a, b) => ({ a: a > +b })} hhh={false} // should allow class/style as attrs class="bar" style={{ color: 'red' }} // should allow key key={'foo'} // should allow ref ref={'foo'} /> ) // @ts-expect-error missing required props expectError() expectError( // @ts-expect-error wrong prop types ) // @ts-expect-error expectError() }) // describe('type inference w/ optional props declaration', () => { // const MyComponent = defineComponent({ // setup(_props: { msg: string }) { // return { // a: 1 // } // }, // render() { // expectType(this.$props.msg) // // props should be readonly // expectError((this.$props.msg = 'foo')) // // should not expose on `this` // expectError(this.msg) // expectType(this.a) // return null // } // }) // expectType() // expectError() // expectError() // }) // describe('type inference w/ direct setup function', () => { // const MyComponent = defineComponent((_props: { msg: string }) => {}) // expectType() // expectError() // expectError() // }) describe('type inference w/ array props declaration', () => { const MyComponent = 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() { 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) } }) expectType() // @ts-expect-error expectError() }) 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() { // props expectType(this.a) // returned from setup() expectType(this.b) // returned from data() expectType(this.c) // computed expectType(this.d) } }) }) 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) => b.length > 1 }, 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')) } }) // without emits defineComponent({ setup(props, { emit }) { emit('test', 1) emit('test') } }) // emit should be valid when ComponentPublicInstance is used. const instance = {} as ComponentPublicInstance instance.$emit('test', 1) instance.$emit('test') }) describe('componentOptions setup should be `SetupContext`', () => { expect({} as ( props: Record, ctx: SetupContext ) => any) })