diff --git a/src/core/util/next-tick.ts b/src/core/util/next-tick.ts index e7a5b006..5e67bd7d 100644 --- a/src/core/util/next-tick.ts +++ b/src/core/util/next-tick.ts @@ -86,7 +86,8 @@ if (typeof Promise !== 'undefined' && isNative(Promise)) { } export function nextTick(): Promise -export function nextTick(cb: (...args: any[]) => any, ctx?: object): void +export function nextTick(this: T, cb: (this: T, ...args: any[]) => any): void +export function nextTick(cb: (this: T, ...args: any[]) => any, ctx: T): void /** * @internal */ diff --git a/types/common.d.ts b/types/common.d.ts index 7b69ddd6..3165711c 100644 --- a/types/common.d.ts +++ b/types/common.d.ts @@ -13,3 +13,9 @@ type Equal = (() => U extends Left ? 1 : 0) extends (() => U extends Right ? 1 : 0) ? true : false; export type HasDefined = Equal extends true ? false : true + +// If the the type T accepts type "any", output type Y, otherwise output type N. +// https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360 +export type IfAny = 0 extends 1 & T ? Y : N + +export type LooseRequired = { [P in string & keyof T]: T[P] } diff --git a/types/index.d.ts b/types/index.d.ts index dd5aab50..91807ae6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -30,7 +30,8 @@ export { VNode, VNodeComponentOptions, VNodeData, - VNodeDirective + VNodeDirective, + ComponentCustomProps } from './vnode' export * from './v3-manual-apis' @@ -47,13 +48,15 @@ export { // v2 already has option with same name and it's for a single computed ComputedOptions as ComponentComputedOptions, MethodOptions as ComponentMethodOptions, - ComponentPropsOptions + ComponentPropsOptions, + ComponentCustomOptions } from './v3-component-options' export { ComponentInstance, ComponentPublicInstance, - ComponentRenderProxy -} from './v3-component-proxy' + CreateComponentPublicInstance, + ComponentCustomProperties +} from './v3-component-public-instance' export { // PropType, // PropOptions, diff --git a/types/jsx.d.ts b/types/jsx.d.ts index c79501ac..151a660b 100644 --- a/types/jsx.d.ts +++ b/types/jsx.d.ts @@ -1313,7 +1313,12 @@ type NativeElements = { > } -import { VNode, VNodeData } from './vnode' +import { + VNode, + VNodeData, + ComponentCustomProps, + AllowedComponentProps +} from './vnode' declare global { namespace JSX { @@ -1329,7 +1334,10 @@ declare global { // @ts-ignore suppress ts:2374 = Duplicate string index signature. [name: string]: any } - interface IntrinsicAttributes extends ReservedProps {} + interface IntrinsicAttributes + extends ReservedProps, + AllowedComponentProps, + ComponentCustomProps {} } } diff --git a/types/options.d.ts b/types/options.d.ts index 80ab87ad..f1db523a 100644 --- a/types/options.d.ts +++ b/types/options.d.ts @@ -2,6 +2,7 @@ import { Vue, CreateElement, CombinedVueInstance } from './vue' import { VNode, VNodeData, VNodeDirective, NormalizedScopedSlot } from './vnode' import { SetupContext } from './v3-setup-context' import { DebuggerEvent } from './v3-generated' +import { DefineComponent } from './v3-define-component' type Constructor = { new (...args: any[]): any @@ -19,6 +20,7 @@ export type Component< | typeof Vue | FunctionalComponentOptions | ComponentOptions + | DefineComponent type EsModule = T | { default: T } @@ -174,7 +176,10 @@ export interface ComponentOptions< el?: Element | string template?: string // hack is for functional component type inference, should not be used in user code - render?(createElement: CreateElement, hack: RenderContext): VNode + render?( + createElement: CreateElement, + hack: RenderContext + ): VNode | null | void renderError?(createElement: CreateElement, err: Error): VNode staticRenderFns?: ((createElement: CreateElement) => VNode)[] @@ -198,6 +203,7 @@ export interface ComponentOptions< [key: string]: | Component | AsyncComponent + | DefineComponent } transitions?: { [key: string]: object } filters?: { [key: string]: Function } diff --git a/types/test/v3/define-component-test.tsx b/types/test/v3/define-component-test.tsx new file mode 100644 index 00000000..7bdbdb82 --- /dev/null +++ b/types/test/v3/define-component-test.tsx @@ -0,0 +1,1080 @@ +import { + Component, + defineComponent, + PropType, + ref, + reactive, + ComponentPublicInstance +} from '../../index' +import { describe, test, expectType, expectError, IsUnion } from '../utils' + +defineComponent({ + props: { + foo: Number + }, + render() { + this.foo + } +}) + +describe('with object props', () => { + interface ExpectedProps { + a?: number | undefined + b: string + e?: Function + h: boolean + j: undefined | (() => string | undefined) + bb: string + bbb: string + bbbb: string | undefined + bbbbb: string | undefined + 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 + ggg: 'foo' | 'bar' + ffff: (a: number, b: string) => { a: boolean } + iii?: (() => string) | (() => number) + jjj: ((arg1: string) => string) | ((arg1: string, arg2: string) => string) + kkk?: any + validated?: string + date?: Date + l?: Date + ll?: Date | number + lll?: string | number + } + + type GT = string & { __brand: unknown } + + const props = { + a: Number, + // required should make property non-void + b: { + type: String, + required: true as true + }, + e: Function, + h: Boolean, + j: Function as PropType string | undefined)>, + // default value should infer type and make it non-void + bb: { + default: 'hello' + }, + bbb: { + // Note: default function value requires arrow syntax + explicit + // annotation + default: (props: any) => (props.bb as string) || 'foo' + }, + bbbb: { + type: String, + default: undefined + }, + bbbbb: { + type: String, + default: () => undefined + }, + // explicit type casting + cc: Array as PropType, + // required + type casting + dd: { + type: Object as PropType<{ n: 1 }>, + required: true as 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 + constructor type casting + ddd: { + type: Array as () => string[], + required: true as true + }, + // required + object return + eee: { + type: Function as PropType<() => { a: string }>, + required: true as true + }, + // required + arguments + object return + fff: { + type: Function as PropType<(a: number, b: string) => { a: boolean }>, + required: true as true + }, + hhh: { + type: Boolean, + required: true as true + }, + // default + type casting + ggg: { + type: String as PropType<'foo' | 'bar'>, + default: 'foo' + }, + // default + function + ffff: { + type: Function as PropType<(a: number, b: string) => { a: boolean }>, + default: (a: number, b: string) => ({ a: a > +b }) + }, + // union + function with different return types + iii: Function as PropType<(() => string) | (() => number)>, + // union + function with different args & same return type + jjj: { + type: Function as PropType< + ((arg1: string) => string) | ((arg1: string, arg2: string) => string) + >, + required: true as true + }, + kkk: null, + validated: { + type: String, + // validator requires explicit annotation + validator: (val: unknown) => val !== '' + }, + date: Date, + l: [Date], + ll: [Date, Number], + lll: [String, Number] + } + + const MyComponent = defineComponent({ + props, + setup(props) { + // type assertion. See https://github.com/SamVerschueren/tsd + expectType(props.a) + expectType(props.b) + expectType(props.e) + expectType(props.h) + expectType(props.j) + expectType(props.bb) + expectType(props.bbb) + expectType(props.bbbb) + expectType(props.bbbbb) + 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) + expectType(props.ggg) + expectType(props.ffff) + if (typeof props.iii !== 'function') { + expectType(props.iii) + } + expectType(props.iii) + expectType>(true) + expectType(props.jjj) + expectType(props.kkk) + expectType(props.validated) + expectType(props.date) + expectType(props.l) + expectType(props.ll) + expectType(props.lll) + + // @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) + }) + } + }, + provide() { + return {} + }, + render() { + const props = this.$props + expectType(props.a) + expectType(props.b) + expectType(props.e) + expectType(props.h) + 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) + expectType(props.ggg) + if (typeof props.iii !== 'function') { + expectType(props.iii) + } + expectType(props.iii) + expectType>(true) + expectType(props.jjj) + expectType(props.kkk) + + // @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.h) + 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) + expectType(this.ggg) + if (typeof this.iii !== 'function') { + expectType(this.iii) + } + expectType(this.iii) + const { jjj } = this + expectType>(true) + expectType(this.jjj) + expectType(this.kkk) + + // @ts-expect-error props on `this` should be readonly + expectError((this.a = 1)) + + // assert setup context unwrapping + expectType(this.c) + expectType(this.d.e.value) + expectType(this.f.g) + + // setup context properties should be mutable + this.c = 2 + + return null + } + }) + + expectType(MyComponent) + + // 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} + ggg="foo" + jjj={() => ''} + // 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 wrong prop types + expectError() + // @ts-expect-error + expectError() +}) + +// describe('type inference w/ optional props declaration', () => { +// const MyComponent = defineComponent<{ a: string[]; msg: string }>({ +// setup(props) { +// expectType(props.msg) +// expectType(props.a) +// return { +// b: 1 +// } +// } +// }) + +// expectType() +// // @ts-expect-error +// expectError() +// // @ts-expect-error +// expectError() +// }) + +// describe('type inference w/ direct setup function', () => { +// const MyComponent = defineComponent((_props: { msg: string }) => {}) +// expectType() +// // @ts-expect-error +// 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, + someRef: ref(0) + } + }, + computed: { + d() { + expectType(this.b) + return this.b + 1 + }, + e: { + get() { + expectType(this.b) + expectType(this.d) + + return this.b + this.d + }, + set(v: number) { + expectType(this.b) + expectType(this.d) + expectType(v) + } + } + }, + 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) + // computed get/set + expectType(this.e) + // expectType(this.someRef) + }, + methods: { + doSomething() { + // props + expectType(this.a) + // returned from setup() + expectType(this.b) + // returned from data() + expectType(this.c) + // computed + expectType(this.d) + // computed get/set + expectType(this.e) + }, + returnSomething() { + return this.a + } + }, + render() { + // props + expectType(this.a) + // returned from setup() + expectType(this.b) + // returned from data() + expectType(this.c) + // computed + expectType(this.d) + // computed get/set + expectType(this.e) + // method + expectType<() => number | undefined>(this.returnSomething) + } + }) +}) + +describe('with mixins', () => { + const MixinA = defineComponent({ + emits: ['bar'], + 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() { + //@ts-expect-error computed are not available on data() + expectError(this.dC1) + //@ts-expect-error computed are not available on data() + expectError(this.dC2) + + return { + d: 4 + } + }, + setup(props) { + expectType(props.aP1) + }, + computed: { + dC1() { + return this.d + this.a + }, + dC2() { + return this.aP1 + 'dC2' + } + } + }) + const MyComponent = defineComponent({ + mixins: [MixinA, MixinB, MixinC, MixinD], + emits: ['click'], + props: { + // required should make property non-void + z: { + type: String, + required: true + } + }, + + data(vm) { + expectType(vm.a) + expectType(vm.b) + expectType(vm.c) + expectType(vm.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) + + return {} + }, + + setup(props) { + expectType(props.z) + // props + // expectType<((...args: any[]) => any) | undefined>(props.onClick) + // from Base + // expectType<((...args: any[]) => any) | undefined>(props.onBar) + expectType(props.aP1) + expectType(props.aP2) + expectType(props.bP1) + expectType(props.bP2) + expectType(props.z) + }, + render() { + const props = this.$props + // props + // expectType<((...args: any[]) => any) | undefined>(props.onClick) + // from Base + // expectType<((...args: any[]) => any) | undefined>(props.onBar) + 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({ + emits: ['bar'], + props: { + mP1: { + type: String, + default: 'mP1' + }, + mP2: Boolean, + mP3: { + type: Boolean, + required: true + } + }, + data() { + return { + a: 1 + } + } + }) + const Base = defineComponent({ + emits: ['foo'], + props: { + p1: Boolean, + p2: { + type: Number, + default: 2 + }, + p3: { + type: Boolean, + required: true + } + }, + data() { + return { + b: 2 + } + }, + computed: { + c(): number { + return this.p2 + this.b + } + } + }) + const MyComponent = defineComponent({ + extends: Base, + mixins: [Mixin], + emits: ['click'], + props: { + // required should make property non-void + z: { + type: String, + required: true + } + }, + render() { + const props = this.$props + // props + // expectType<((...args: any[]) => any) | undefined>(props.onClick) + // from Mixin + // expectType<((...args: any[]) => any) | undefined>(props.onBar) + // from Base + // expectType<((...args: any[]) => any) | undefined>(props.onFoo) + 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() + + // mP1, mP2, p1, and p2 have default value. these are not required + expectType() + + // missing required props + // @ts-expect-error + expectError() + // missing required props from mixin + // @ts-expect-error + expectError() + // missing required props from extends + // @ts-expect-error + expectError() + + // wrong prop types + // @ts-expect-error + expectError() + // @ts-expect-error + expectError() + + // #3468 + const CompWithD = defineComponent({ + data() { + return { foo: 1 } + } + }) + const CompWithC = defineComponent({ + computed: { + foo() { + return 1 + } + } + }) + const CompWithM = defineComponent({ methods: { foo() {} } }) + const CompEmpty = defineComponent({}) + + defineComponent({ + mixins: [CompWithD, CompEmpty], + mounted() { + expectType(this.foo) + } + }) + defineComponent({ + mixins: [CompWithC, CompEmpty], + mounted() { + expectType(this.foo) + } + }) + defineComponent({ + mixins: [CompWithM, CompEmpty], + mounted() { + expectType<() => void>(this.foo) + } + }) +}) + +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 }) { + // expectType<((n: number) => boolean) | undefined>(props.onClick) + // expectType<((b: string) => boolean) | undefined>(props.onInput) + 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)) + }, + mounted() { + // #3599 + this.$nextTick(function () { + // this should be bound to this instance + 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 }) { + // expectType<((...args: any[]) => any) | undefined>(props.onFoo) + // expectType<((...args: any[]) => any) | undefined>(props.onBar) + 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')) + } + }) + + // with tsx + const Component = defineComponent({ + emits: { + click: (n: number) => typeof n === 'number' + }, + setup(props, { emit }) { + // expectType<((n: number) => any) | undefined>(props.onClick) + emit('click', 1) + // @ts-expect-error + expectError(emit('click')) + // @ts-expect-error + expectError(emit('click', 'foo')) + } + }) + + // defineComponent({ + // render() { + // return ( + // { + // return n + 1 + // }} + // /> + // ) + // } + // }) + + // 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') + + // `this` should be void inside of emits validators + defineComponent({ + props: ['bar'], + emits: { + foo(): boolean { + // @ts-expect-error + return this.bar === 3 + } + } + }) +}) + +// describe('componentOptions setup should be `SetupContext`', () => { +// expectType( +// {} as (props: Record, ctx: SetupContext) => any +// ) +// }) + +describe('extract instance type', () => { + const Base = defineComponent({ + props: { + baseA: { + type: Number, + default: 1 + } + } + }) + const MixinA = defineComponent({ + props: { + mA: { + type: String, + default: '' + } + } + }) + const CompA = defineComponent({ + extends: Base, + mixins: [MixinA], + props: { + a: { + type: Boolean, + default: false + }, + b: { + type: String, + required: true + }, + c: Number + } + }) + + const compA = {} as InstanceType + + expectType(compA.a) + expectType(compA.b) + expectType(compA.c) + // mixins + expectType(compA.mA) + // extends + expectType(compA.baseA) + + // @ts-expect-error + expectError((compA.a = true)) + // @ts-expect-error + expectError((compA.b = 'foo')) + // @ts-expect-error + expectError((compA.c = 1)) + // @ts-expect-error + expectError((compA.mA = 'foo')) + // @ts-expect-error + expectError((compA.baseA = 1)) +}) + +// #5948 +describe('DefineComponent should infer correct types when assigning to Component', () => { + let component: Component + component = defineComponent({ + setup(_, { attrs, slots }) { + // @ts-expect-error should not be any + expectType<[]>(attrs) + // @ts-expect-error should not be any + expectType<[]>(slots) + } + }) + expectType(component) +}) + +// #5969 +describe('should allow to assign props', () => { + const Child = defineComponent({ + props: { + bar: String + } + }) + + const Parent = defineComponent({ + props: { + ...Child.props, + foo: String + } + }) + + const child = new Child() + expectType() +}) + +// check if defineComponent can be exported +export default { + // no props + b: defineComponent({ + data() { + return {} + } + }), + c: defineComponent({ + props: ['a'] + }), + d: defineComponent({ + props: { + a: Number + } + }) +} diff --git a/types/v3-component-options.d.ts b/types/v3-component-options.d.ts index d6e63ed8..d8c64ab1 100644 --- a/types/v3-component-options.d.ts +++ b/types/v3-component-options.d.ts @@ -2,11 +2,33 @@ import { Vue } from './vue' import { VNode } from './vnode' import { ComponentOptions as Vue2ComponentOptions } from './options' import { EmitsOptions, SetupContext } from './v3-setup-context' -import { Data } from './common' -import { ComponentPropsOptions, ExtractPropTypes } from './v3-component-props' -import { ComponentRenderProxy } from './v3-component-proxy' +import { Data, LooseRequired, UnionToIntersection } from './common' +import { + ComponentPropsOptions, + ExtractDefaultPropTypes, + ExtractPropTypes +} from './v3-component-props' +import { CreateComponentPublicInstance } from './v3-component-public-instance' export { ComponentPropsOptions } from './v3-component-props' +/** + * Interface for declaring custom options. + * + * @example + * ```ts + * declare module 'vue' { + * interface ComponentCustomOptions { + * beforeRouteUpdate?( + * to: Route, + * from: Route, + * next: () => void + * ): void + * } + * } + * ``` + */ +export interface ComponentCustomOptions {} + export type ComputedGetter = (ctx?: any) => T export type ComputedSetter = (v: T) => void @@ -34,24 +56,76 @@ export type SetupFunction< ctx: SetupContext ) => RawBindings | (() => VNode | null) | void -interface ComponentOptionsBase< - Props, - D = Data, - C extends ComputedOptions = {}, - M extends MethodOptions = {} -> extends Omit< - Vue2ComponentOptions, - 'data' | 'computed' | 'method' | 'setup' | 'props' - > { - // allow any custom options - [key: string]: any +type ExtractOptionProp = T extends ComponentOptionsBase< + infer P, // Props + any, // RawBindings + any, // D + any, // C + any, // M + any, // Mixin + any, // Extends + any, // EmitsOptions + any // Defaults +> + ? unknown extends P + ? {} + : P + : {} +export interface ComponentOptionsBase< + Props, + RawBindings, + D, + C extends ComputedOptions, + M extends MethodOptions, + Mixin extends ComponentOptionsMixin, + Extends extends ComponentOptionsMixin, + Emits extends EmitsOptions, + EmitNames extends string = string, + Defaults = {} +> extends Omit< + Vue2ComponentOptions, + 'data' | 'computed' | 'methods' | 'setup' | 'props' | 'mixins' | 'extends' + >, + ComponentCustomOptions { // rewrite options api types - data?: (this: Props & Vue, vm: Props) => D + data?: ( + this: CreateComponentPublicInstance, + vm: CreateComponentPublicInstance + ) => D computed?: C methods?: M + mixins?: Mixin[] + extends?: Extends + emits?: (Emits | EmitNames[]) & ThisType + setup?: SetupFunction< + Readonly< + LooseRequired< + Props & + UnionToIntersection> & + UnionToIntersection> + > + >, + RawBindings, + Emits + > + + __defaults?: Defaults } +export type ComponentOptionsMixin = ComponentOptionsBase< + any, + any, + any, + any, + any, + any, + any, + any, + any, + any +> + export type ExtractComputedReturns = { [key in keyof T]: T[key] extends { get: (...args: any[]) => infer TReturn } ? TReturn @@ -66,17 +140,36 @@ export type ComponentOptionsWithProps< D = Data, C extends ComputedOptions = {}, M extends MethodOptions = {}, - Mixin = {}, - Extends = {}, + Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, + Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Emits extends EmitsOptions = {}, EmitsNames extends string = string, - Props = ExtractPropTypes -> = ComponentOptionsBase & { + Props = ExtractPropTypes, + Defaults = ExtractDefaultPropTypes +> = ComponentOptionsBase< + Props, + RawBindings, + D, + C, + M, + Mixin, + Extends, + Emits, + EmitsNames, + Defaults +> & { props?: PropsOptions - emits?: (Emits | EmitsNames[]) & ThisType - setup?: SetupFunction } & ThisType< - ComponentRenderProxy + CreateComponentPublicInstance< + Props, + RawBindings, + D, + C, + M, + Mixin, + Extends, + Emits + > > export type ComponentOptionsWithArrayProps< @@ -85,17 +178,35 @@ export type ComponentOptionsWithArrayProps< D = Data, C extends ComputedOptions = {}, M extends MethodOptions = {}, - Mixin = {}, - Extends = {}, + Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, + Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Emits extends EmitsOptions = {}, EmitsNames extends string = string, Props = Readonly<{ [key in PropNames]?: any }> -> = ComponentOptionsBase & { +> = ComponentOptionsBase< + Props, + RawBindings, + D, + C, + M, + Mixin, + Extends, + Emits, + EmitsNames, + {} +> & { props?: PropNames[] - emits?: (Emits | EmitsNames[]) & ThisType - setup?: SetupFunction } & ThisType< - ComponentRenderProxy + CreateComponentPublicInstance< + Props, + RawBindings, + D, + C, + M, + Mixin, + Extends, + Emits + > > export type ComponentOptionsWithoutProps< @@ -104,16 +215,34 @@ export type ComponentOptionsWithoutProps< D = Data, C extends ComputedOptions = {}, M extends MethodOptions = {}, - Mixin = {}, - Extends = {}, + Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, + Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Emits extends EmitsOptions = {}, EmitsNames extends string = string -> = ComponentOptionsBase & { +> = ComponentOptionsBase< + Props, + RawBindings, + D, + C, + M, + Mixin, + Extends, + Emits, + EmitsNames, + {} +> & { props?: undefined - emits?: (Emits | EmitsNames[]) & ThisType - setup?: SetupFunction } & ThisType< - ComponentRenderProxy + CreateComponentPublicInstance< + Props, + RawBindings, + D, + C, + M, + Mixin, + Extends, + Emits + > > export type WithLegacyAPI = T & diff --git a/types/v3-component-props.d.ts b/types/v3-component-props.d.ts index f4d0a787..f0b2b706 100644 --- a/types/v3-component-props.d.ts +++ b/types/v3-component-props.d.ts @@ -1,4 +1,4 @@ -import { Data } from './common' +import { Data, IfAny } from './common' export type ComponentPropsOptions

= | ComponentObjectPropsOptions

@@ -48,26 +48,25 @@ type ExtractCorrectPropType = T extends Function ? ExtractFunctionPropType : Exclude -// prettier-ignore -type InferPropType = T extends null +type InferPropType = [T] extends [null] ? any // null & true would fail to infer - : T extends { type: null | true } - ? any // As TS issue https://github.com/Microsoft/TypeScript/issues/14829 // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` // `BooleanConstructor` when inferred from PropConstructor(with PropMethod) becomes `Boolean` - : T extends ObjectConstructor | { type: ObjectConstructor } - ? Record - : T extends BooleanConstructor | { type: BooleanConstructor } - ? boolean - : T extends DateConstructor | { type: DateConstructor} - ? Date - : T extends FunctionConstructor - ? Function - : T extends Prop - ? unknown extends V - ? D extends null | undefined - ? V - : D - : ExtractCorrectPropType - : T + : [T] extends [{ type: null | true }] + ? any // As TS issue https://github.com/Microsoft/TypeScript/issues/14829 // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` // `BooleanConstructor` when inferred from PropConstructor(with PropMethod) becomes `Boolean` + : [T] extends [ObjectConstructor | { type: ObjectConstructor }] + ? Record + : [T] extends [BooleanConstructor | { type: BooleanConstructor }] + ? boolean + : [T] extends [DateConstructor | { type: DateConstructor }] + ? Date + : [T] extends [(infer U)[] | { type: (infer U)[] }] + ? U extends DateConstructor + ? Date | InferPropType + : InferPropType + : [T] extends [Prop] + ? unknown extends V + ? IfAny + : V + : T export type ExtractPropTypes = { // use `keyof Pick>` instead of `RequiredKeys` to support IDE features diff --git a/types/v3-component-proxy.d.ts b/types/v3-component-proxy.d.ts deleted file mode 100644 index d1f73452..00000000 --- a/types/v3-component-proxy.d.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { ExtractDefaultPropTypes, ExtractPropTypes } from './v3-component-props' -import { - nextTick, - ShallowUnwrapRef, - UnwrapNestedRefs, - WatchOptions, - WatchStopHandle -} from './v3-generated' -import { Data } from './common' - -import { Vue, VueConstructor } from './vue' -import { ComponentOptions as Vue2ComponentOptions } from './options' -import { - ComputedOptions, - MethodOptions, - ExtractComputedReturns -} from './v3-component-options' -import { - ComponentRenderEmitFn, - EmitFn, - EmitsOptions, - ObjectEmitsOptions, - Slots -} from './v3-setup-context' - -type EmitsToProps = T extends string[] - ? { - [K in string & `on${Capitalize}`]?: (...args: any[]) => any - } - : T extends ObjectEmitsOptions - ? { - [K in string & - `on${Capitalize}`]?: K extends `on${infer C}` - ? T[Uncapitalize] extends null - ? (...args: any[]) => any - : ( - ...args: T[Uncapitalize] extends (...args: infer P) => any - ? P - : never - ) => any - : never - } - : {} - -export type ComponentInstance = InstanceType - -// public properties exposed on the proxy, which is used as the render context -// in templates (as `this` in the render option) -export type ComponentRenderProxy< - P = {}, // props type extracted from props option - B = {}, // raw bindings returned from setup() - D = {}, // return from data() - C extends ComputedOptions = {}, - M extends MethodOptions = {}, - Mixin = {}, - Extends = {}, - Emits extends EmitsOptions = {}, - PublicProps = P, - Defaults = {}, - MakeDefaultsOptional extends boolean = false -> = { - $data: D - $props: Readonly< - MakeDefaultsOptional extends true - ? Partial & Omit

- : P & PublicProps - > - $attrs: Record - $emit: ComponentRenderEmitFn< - Emits, - keyof Emits, - ComponentRenderProxy< - P, - B, - D, - C, - M, - Mixin, - Extends, - Emits, - PublicProps, - Defaults, - MakeDefaultsOptional - > - > -} & Readonly

& - ShallowUnwrapRef & - D & - M & - ExtractComputedReturns & - Omit - -// for Vetur and TSX support -type VueConstructorProxy< - PropsOptions, - RawBindings, - Data, - Computed extends ComputedOptions, - Methods extends MethodOptions, - Mixin = {}, - Extends = {}, - Emits extends EmitsOptions = {}, - Props = ExtractPropTypes & - ({} extends Emits ? {} : EmitsToProps) -> = Omit & { - new (...args: any[]): ComponentRenderProxy< - Props, - ShallowUnwrapRef, - Data, - Computed, - Methods, - Mixin, - Extends, - Emits, - Props, - ExtractDefaultPropTypes, - true - > -} - -type DefaultData = object | ((this: V) => object) -type DefaultMethods = { [key: string]: (this: V, ...args: any[]) => any } -type DefaultComputed = { [key: string]: any } - -export type VueProxy< - PropsOptions, - RawBindings, - Data = DefaultData, - Computed extends ComputedOptions = DefaultComputed, - Methods extends MethodOptions = DefaultMethods, - Mixin = {}, - Extends = {}, - Emits extends EmitsOptions = {} -> = Vue2ComponentOptions< - Vue, - ShallowUnwrapRef & Data, - Methods, - Computed, - PropsOptions, - ExtractPropTypes -> & - VueConstructorProxy< - PropsOptions, - RawBindings, - Data, - Computed, - Methods, - Mixin, - Extends, - Emits - > - -// public properties exposed on the proxy, which is used as the render context -// in templates (as `this` in the render option) -export type ComponentPublicInstance< - P = {}, // props type extracted from props option - B = {}, // raw bindings returned from setup() - D = {}, // return from data() - C extends ComputedOptions = {}, - M extends MethodOptions = {}, - E extends EmitsOptions = {}, - PublicProps = P, - Defaults = {}, - MakeDefaultsOptional extends boolean = false -> = { - $data: D - $props: MakeDefaultsOptional extends true - ? Partial & Omit

- : P & PublicProps - $attrs: Data - $refs: Data - $slots: Slots - $root: ComponentPublicInstance | null - $parent: ComponentPublicInstance | null - $emit: EmitFn - $el: any - // $options: Options & MergedComponentOptionsOverride - $forceUpdate: () => void - $nextTick: typeof nextTick - $watch( - source: string | Function, - cb: Function, - options?: WatchOptions - ): WatchStopHandle -} & P & - ShallowUnwrapRef & - UnwrapNestedRefs & - ExtractComputedReturns & - M diff --git a/types/v3-component-public-instance.d.ts b/types/v3-component-public-instance.d.ts new file mode 100644 index 00000000..6734c596 --- /dev/null +++ b/types/v3-component-public-instance.d.ts @@ -0,0 +1,230 @@ +import { ExtractDefaultPropTypes, ExtractPropTypes } from './v3-component-props' +import { + DebuggerEvent, + nextTick, + ShallowUnwrapRef, + UnwrapNestedRefs, + WatchOptions, + WatchStopHandle +} from './v3-generated' +import { Data, UnionToIntersection } from './common' + +import { VueConstructor } from './vue' +import { + ComputedOptions, + MethodOptions, + ExtractComputedReturns, + ComponentOptionsMixin, + ComponentOptionsBase +} from './v3-component-options' +import { EmitFn, EmitsOptions, Slots } from './v3-setup-context' + +/** + * Custom properties added to component instances in any way and can be accessed through `this` + * + * @example + * ```ts + * import { Router } from 'vue-router' + * + * declare module 'vue' { + * interface ComponentCustomProperties { + * $router: Router + * } + * } + * ``` + */ +export interface ComponentCustomProperties {} + +export type ComponentInstance = InstanceType + +export type OptionTypesKeys = 'P' | 'B' | 'D' | 'C' | 'M' | 'Defaults' + +export type OptionTypesType< + P = {}, + B = {}, + D = {}, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Defaults = {} +> = { + P: P + B: B + D: D + C: C + M: M + Defaults: Defaults +} + +type IsDefaultMixinComponent = T extends ComponentOptionsMixin + ? ComponentOptionsMixin extends T + ? true + : false + : false + +type MixinToOptionTypes = T extends ComponentOptionsBase< + infer P, + infer B, + infer D, + infer C, + infer M, + infer Mixin, + infer Extends, + any, + any, + infer Defaults +> + ? OptionTypesType

& + IntersectionMixin & + IntersectionMixin + : never + +// ExtractMixin(map type) is used to resolve circularly references +type ExtractMixin = { + Mixin: MixinToOptionTypes +}[T extends ComponentOptionsMixin ? 'Mixin' : never] + +type IntersectionMixin = IsDefaultMixinComponent extends true + ? OptionTypesType<{}, {}, {}, {}, {}, {}> + : UnionToIntersection> + +type UnwrapMixinsType< + T, + Type extends OptionTypesKeys +> = T extends OptionTypesType ? T[Type] : never + +type EnsureNonVoid = T extends void ? {} : T + +export type CreateComponentPublicInstance< + P = {}, + B = {}, + D = {}, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, + Extends extends ComponentOptionsMixin = ComponentOptionsMixin, + E extends EmitsOptions = {}, + PublicProps = P, + Defaults = {}, + MakeDefaultsOptional extends boolean = false, + PublicMixin = IntersectionMixin & IntersectionMixin, + PublicP = UnwrapMixinsType & EnsureNonVoid

, + PublicB = UnwrapMixinsType & EnsureNonVoid, + PublicD = UnwrapMixinsType & EnsureNonVoid, + PublicC extends ComputedOptions = UnwrapMixinsType & + EnsureNonVoid, + PublicM extends MethodOptions = UnwrapMixinsType & + EnsureNonVoid, + PublicDefaults = UnwrapMixinsType & + EnsureNonVoid +> = ComponentPublicInstance< + PublicP, + PublicB, + PublicD, + PublicC, + PublicM, + E, + PublicProps, + PublicDefaults, + MakeDefaultsOptional +> + +// public properties exposed on the proxy, which is used as the render context +// in templates (as `this` in the render option) +export type ComponentPublicInstance< + P = {}, // props type extracted from props option + B = {}, // raw bindings returned from setup() + D = {}, // return from data() + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + E extends EmitsOptions = {}, + PublicProps = P, + Defaults = {}, + MakeDefaultsOptional extends boolean = false, + Options = ComponentOptionsBase< + any, + any, + any, + any, + any, + any, + any, + any, + any, + any + > +> = { + // $: ComponentInternalInstance + $data: D + $props: Readonly< + MakeDefaultsOptional extends true + ? Partial & Omit

+ : P & PublicProps + > + $attrs: Data + $refs: Data + $slots: Slots + $root: ComponentPublicInstance | null + $parent: ComponentPublicInstance | null + $emit: EmitFn + $el: any + $options: Options & MergedComponentOptionsOverride + $forceUpdate: () => void + $nextTick: typeof nextTick + $watch( + source: string | Function, + cb: Function, + options?: WatchOptions + ): WatchStopHandle +} & Readonly

& + ShallowUnwrapRef & + UnwrapNestedRefs & + ExtractComputedReturns & + M & + ComponentCustomProperties + +type MergedHook void> = T | T[] + +export type MergedComponentOptionsOverride = { + beforeCreate?: MergedHook + created?: MergedHook + beforeMount?: MergedHook + mounted?: MergedHook + beforeUpdate?: MergedHook + updated?: MergedHook + activated?: MergedHook + deactivated?: MergedHook + /** @deprecated use `beforeUnmount` instead */ + beforeDestroy?: MergedHook + beforeUnmount?: MergedHook + /** @deprecated use `unmounted` instead */ + destroyed?: MergedHook + unmounted?: MergedHook + renderTracked?: MergedHook + renderTriggered?: MergedHook + errorCaptured?: MergedHook +} + +export type DebuggerHook = (e: DebuggerEvent) => void + +export type ErrorCapturedHook = ( + err: TError, + instance: ComponentPublicInstance | null, + info: string +) => boolean | void + +export type ComponentPublicInstanceConstructor< + T extends ComponentPublicInstance< + Props, + RawBindings, + D, + C, + M + > = ComponentPublicInstance, + Props = any, + RawBindings = any, + D = any, + C extends ComputedOptions = ComputedOptions, + M extends MethodOptions = MethodOptions +> = { + new (...args: any[]): T +} diff --git a/types/v3-define-component.d.ts b/types/v3-define-component.d.ts index 8d3a4780..d6e73914 100644 --- a/types/v3-define-component.d.ts +++ b/types/v3-define-component.d.ts @@ -1,15 +1,72 @@ -import { ComponentPropsOptions } from './v3-component-props' +import { Component } from '..' +import { + ComponentPropsOptions, + ExtractDefaultPropTypes, + ExtractPropTypes +} from './v3-component-props' import { MethodOptions, ComputedOptions, ComponentOptionsWithoutProps, ComponentOptionsWithArrayProps, - ComponentOptionsWithProps + ComponentOptionsWithProps, + ComponentOptionsMixin, + ComponentOptionsBase } from './v3-component-options' -import { VueProxy } from './v3-component-proxy' +import { + ComponentPublicInstanceConstructor, + CreateComponentPublicInstance +} from './v3-component-public-instance' import { Data, HasDefined } from './common' import { EmitsOptions } from './v3-setup-context' +type DefineComponent< + PropsOrPropOptions = {}, + RawBindings = {}, + D = {}, + C extends ComputedOptions = ComputedOptions, + M extends MethodOptions = MethodOptions, + Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, + Extends extends ComponentOptionsMixin = ComponentOptionsMixin, + E extends EmitsOptions = {}, + EE extends string = string, + Props = Readonly< + PropsOrPropOptions extends ComponentPropsOptions + ? ExtractPropTypes + : PropsOrPropOptions + >, + Defaults = ExtractDefaultPropTypes +> = ComponentPublicInstanceConstructor< + CreateComponentPublicInstance< + Props, + RawBindings, + D, + C, + M, + Mixin, + Extends, + E, + Props, + Defaults, + true + > & + Props +> & + ComponentOptionsBase< + Props, + RawBindings, + D, + C, + M, + Mixin, + Extends, + E, + EE, + Defaults + > & { + props: PropsOrPropOptions + } + /** * overload 1: object format with no props */ @@ -18,8 +75,8 @@ export function defineComponent< D = Data, C extends ComputedOptions = {}, M extends MethodOptions = {}, - Mixin = {}, - Extends = {}, + Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, + Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Emits extends EmitsOptions = {}, EmitsNames extends string = string >( @@ -34,7 +91,8 @@ export function defineComponent< Emits, EmitsNames > -): VueProxy<{}, RawBindings, D, C, M, Mixin, Extends, Emits> +): DefineComponent<{}, RawBindings, D, C, M, Mixin, Extends, Emits> + /** * overload 2: object format with array props declaration * props inferred as `{ [key in PropNames]?: any }` @@ -47,8 +105,8 @@ export function defineComponent< D = Data, C extends ComputedOptions = {}, M extends MethodOptions = {}, - Mixin = {}, - Extends = {}, + Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, + Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Emits extends EmitsOptions = {}, EmitsNames extends string = string, PropsOptions extends ComponentPropsOptions = ComponentPropsOptions @@ -64,7 +122,7 @@ export function defineComponent< Emits, EmitsNames > -): VueProxy< +): DefineComponent< Readonly<{ [key in PropNames]?: any }>, RawBindings, D, @@ -86,8 +144,8 @@ export function defineComponent< D = Data, C extends ComputedOptions = {}, M extends MethodOptions = {}, - Mixin = {}, - Extends = {}, + Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, + Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Emits extends EmitsOptions = {}, EmitsNames extends string = string, PropsOptions extends ComponentPropsOptions = ComponentPropsOptions @@ -116,4 +174,4 @@ export function defineComponent< Emits, EmitsNames > -): VueProxy +): DefineComponent diff --git a/types/v3-setup-context.d.ts b/types/v3-setup-context.d.ts index 43c412e6..8a0a7822 100644 --- a/types/v3-setup-context.d.ts +++ b/types/v3-setup-context.d.ts @@ -29,12 +29,6 @@ export type EmitFn< }[Event] > -export type ComponentRenderEmitFn< - Options = ObjectEmitsOptions, - Event extends keyof Options = keyof Options, - T extends Vue | void = void -> = EmitFn - export interface SetupContext { attrs: Data slots: Slots diff --git a/types/vnode.d.ts b/types/vnode.d.ts index fabfad44..7a543f0a 100644 --- a/types/vnode.d.ts +++ b/types/vnode.d.ts @@ -1,6 +1,19 @@ import { Vue } from './vue' import { DirectiveFunction, DirectiveOptions } from './options' +/** + * For extending allowed non-declared props on components in TSX + */ +export interface ComponentCustomProps {} + +/** + * Default allowed non-declared props on component in TSX + */ +export interface AllowedComponentProps { + class?: unknown + style?: unknown +} + export type ScopedSlot = (props: any) => ScopedSlotReturnValue type ScopedSlotReturnValue = | VNode