Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(types): support inferring attrs #13089

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion types/index.d.ts
Expand Up @@ -49,7 +49,8 @@ export {
ComputedOptions as ComponentComputedOptions,
MethodOptions as ComponentMethodOptions,
ComponentPropsOptions,
ComponentCustomOptions
ComponentCustomOptions,
AttrsType
} from './v3-component-options'
export {
ComponentInstance,
Expand Down
8 changes: 6 additions & 2 deletions types/options.d.ts
Expand Up @@ -3,8 +3,9 @@ import { VNode, VNodeData, VNodeDirective, NormalizedScopedSlot } from './vnode'
import { SetupContext } from './v3-setup-context'
import { DebuggerEvent } from './v3-generated'
import { DefineComponent } from './v3-define-component'
import { ComponentOptionsMixin } from './v3-component-options'
import { AttrsType, ComponentOptionsMixin, noAttrsDefine, UnwrapAttrsType } from './v3-component-options'
import { ObjectDirective, FunctionDirective } from './v3-directive'
import { Data } from './common'

type Constructor = {
new (...args: any[]): any
Expand Down Expand Up @@ -263,7 +264,7 @@ export interface FunctionalComponentOptions<
): VNode | VNode[]
}

export interface RenderContext<Props = DefaultProps> {
export interface RenderContext<Props = DefaultProps, Attrs extends AttrsType = Record<string, unknown>> {
props: Props
children: VNode[]
slots(): any
Expand All @@ -272,6 +273,9 @@ export interface RenderContext<Props = DefaultProps> {
listeners: { [key: string]: Function | Function[] }
scopedSlots: { [key: string]: NormalizedScopedSlot }
injections: any
attrs: noAttrsDefine<Attrs> extends true
? Data
: UnwrapAttrsType<NonNullable<Attrs>>
}

export type Prop<T> =
Expand Down
200 changes: 196 additions & 4 deletions types/test/v3/define-component-test.tsx
@@ -1,13 +1,14 @@
import Vue, { VueConstructor } from '../../index'
import Vue, { AttrsType, ExtractPropTypes, VueConstructor } from '../../index'
import {
Component,
defineComponent,
PropType,
ref,
reactive,
ComponentPublicInstance
} from '../../index'
import { reactive } from '../../../src/v3/reactivity/reactive'
import { ref } from '../../../src/v3/reactivity/ref'
import { describe, test, expectType, expectError, IsUnion } from '../utils'
import { ImgHTMLAttributes, StyleValue } from '../../jsx'

describe('compat with v2 APIs', () => {
const comp = defineComponent({})
Expand Down Expand Up @@ -1079,7 +1080,9 @@ export default {
}
})
}

const Foo = defineComponent((props: { msg: string }) => {
return () => <div>{props.msg}</div>
})
describe('functional w/ array props', () => {
const Foo = defineComponent({
functional: true,
Expand Down Expand Up @@ -1225,3 +1228,192 @@ describe('should report non-existent properties in instance', () => {
// @ts-expect-error
instance2.foo
})
const MyComp = defineComponent({
props: {
foo: String
},

created() {
console.log(this)
}
})
describe('define attrs', () => {
test('define attrs w/ object props', () => {
const MyComp = defineComponent({
props: {
foo: String
},
attrs: Object as AttrsType<{
bar?: number
}>,
created() {
expectType<number | undefined>(this.$attrs.bar)
}
})
expectType<JSX.Element>(<MyComp foo="1" bar={1} />)
})

test('define attrs w/ array props', () => {
const MyComp = defineComponent({
props: ['foo'],
attrs: Object as AttrsType<{
bar?: number
}>,
created() {
expectType<number | undefined>(this.$attrs.bar)
}
})
expectType<JSX.Element>(<MyComp foo="1" bar={1} />)
})

test('define attrs w/ no props', () => {
const MyComp = defineComponent({
attrs: Object as AttrsType<{
bar?: number
}>,
created() {
expectType<number | undefined>(this.$attrs.bar)
}
})
expectType<JSX.Element>(<MyComp bar={1} />)
})

test('define attrs w/ composition api', () => {
const MyComp = defineComponent({
props: {
foo: {
type: String,
required: true
}
},
attrs: Object as AttrsType<{
bar?: number
}>,
setup(props, { attrs }) {
expectType<string>(props.foo)
expectType<number | undefined>(attrs.bar)
}
})
expectType<JSX.Element>(<MyComp foo="1" bar={1} />)
})

test('functional w/ array props', () => {
const Comp = defineComponent({
functional: true,
props: ['foo'],
attrs: Object as AttrsType<{
bar?: number
}>,
render(h, ctx) {
expectType<any>(ctx.props.foo)
expectType<number | undefined>(ctx.attrs.bar)
}
});

<Comp foo="hi" bar={1}/>
})

test('functional w/ object props', () => {
const Comp = defineComponent({
functional: true,
props: {
foo: String
},
attrs: Object as AttrsType<{
bar?: number
}>,
render(h, ctx) {
expectType<any>(ctx.props.foo)
expectType<number | undefined>(ctx.attrs.bar)
}
});
<Comp foo="hi" bar={1}/>
})

test('define attrs as low priority', () => {
const MyComp = defineComponent({
props: {
foo: String
},
attrs: Object as AttrsType<{
foo?: number
}>,
created() {
// @ts-expect-error
this.$attrs.foo

expectType<string | undefined>(this.foo)
}
})
expectType<JSX.Element>(<MyComp foo="1" />)
})

test('define required attrs', () => {
const MyComp = defineComponent({
attrs: Object as AttrsType<{
bar: number
}>,
created() {
expectType<number | undefined>(this.$attrs.bar)
}
})
expectType<JSX.Element>(<MyComp bar={1} />)
// @ts-expect-error
expectType<JSX.Element>(<MyComp />)
})

test('define no attrs w/ object props', () => {
const MyComp = defineComponent({
props: {
foo: String
},
created() {
expectType<unknown>(this.$attrs.bar)
}
})
// @ts-expect-error
expectType<JSX.Element>(<MyComp foo="1" bar={1} />)
})

test('wrap elements, such as img element', () => {
const MyImg = defineComponent({
props: {
foo: String
},
attrs: Object as AttrsType<ImgHTMLAttributes>,
created() {
expectType<any>(this.$attrs.class)
expectType<StyleValue | undefined>(this.$attrs.style)
},
render() {
return <img {...this.$attrs} />
}
})
expectType<JSX.Element>(<MyImg class={'str'} style={'str'} src={'str'} />)
})

test('secondary packaging of components', () => {
const childProps = {
foo: String
}
type ChildProps = ExtractPropTypes<typeof childProps>
const Child = defineComponent({
props: childProps,
render() {
return <div>{this.foo}</div>
}
})
const Comp = defineComponent({
props: {
bar: Number
},
attrs: Object as AttrsType<ChildProps>,
render() {
return <Child {...this.$attrs} />
}
})
expectType<JSX.Element>(
<Comp class={'str'} style={'str'} bar={1} foo={'str'} />
)
})
})
3 changes: 2 additions & 1 deletion types/umd.d.ts
@@ -1,4 +1,5 @@
import * as V from './index'
import { AttrsType } from './index'
import {
DefaultData,
DefaultProps,
Expand Down Expand Up @@ -38,7 +39,7 @@ declare namespace Vue {
Props = DefaultProps,
PropDefs = PropsDefinition<Props>
> = V.FunctionalComponentOptions<Props, PropDefs>
export type RenderContext<Props = DefaultProps> = V.RenderContext<Props>
export type RenderContext<Props = DefaultProps, Attrs extends AttrsType = Record<string, unknown>> = V.RenderContext<Props, Attrs>
export type PropType<T> = V.PropType<T>
export type PropOptions<T = any> = V.PropOptions<T>
export type ComputedOptions<T> = V.ComputedOptions<T>
Expand Down