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

types(slots): Add typed slots #2693

Closed
wants to merge 12 commits into from
Original file line number Diff line number Diff line change
@@ -739,6 +739,71 @@ return { a, props, emit }
}"
`;

exports[`SFC compile <script setup> > defineProps/defineSlots in multi-variable declaration (full removal) 1`] = `
"export default {
props: ['item'],
slots: ['a'],
setup(__props, { expose, slots }) {
expose();

const props = __props;



return { props, slots }
}

}"
`;

exports[`SFC compile <script setup> > defineProps/defineSlots in multi-variable declaration 1`] = `
"export default {
props: ['item'],
slots: ['a'],
setup(__props, { expose, slots }) {
expose();

const props = __props;

const a = 1;

return { props, a, slots }
}

}"
`;

exports[`SFC compile <script setup> > defineProps/defineSlots in multi-variable declaration fix #6757 1`] = `
"export default {
props: ['item'],
slots: ['a'],
setup(__props, { expose, slots }) {
expose();

const props = __props;

const a = 1;

return { a, props, slots }
}

}"
`;

exports[`SFC compile <script setup> > defineSlots() 1`] = `
"export default {
slots: ['foo', 'bar'],
setup(__props, { expose, slots: mySlots }) {
expose();



return { mySlots }
}

}"
`;

exports[`SFC compile <script setup> > dev mode import usage check > TS annotations 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
import { Foo, Bar, Baz, Qux, Fred } from './x'
62 changes: 60 additions & 2 deletions packages/compiler-sfc/__tests__/compileScript.spec.ts
Original file line number Diff line number Diff line change
@@ -172,6 +172,65 @@ const myEmit = defineEmits(['foo', 'bar'])
expect(content).toMatch(`emits: ['a'],`)
})

test('defineSlots()', () => {
const { content, bindings } = compile(`
<script setup>
const mySlots = defineSlots(['foo', 'bar'])
</script>
`)
assertCode(content)
expect(bindings).toStrictEqual({
mySlots: BindingTypes.SETUP_CONST
})
// should remove defineOptions import and call
expect(content).not.toMatch('defineSlots')
// should generate correct setup signature
expect(content).toMatch(`setup(__props, { expose, slots: mySlots }) {`)
// should include context options in default export
expect(content).toMatch(`export default {
slots: ['foo', 'bar'],`)
})

test('defineProps/defineSlots in multi-variable declaration', () => {
const { content } = compile(`
<script setup>
const props = defineProps(['item']),
a = 1,
slots = defineSlots(['a']);
</script>
`)
assertCode(content)
expect(content).toMatch(`const a = 1;`) // test correct removal
expect(content).toMatch(`props: ['item'],`)
expect(content).toMatch(`slots: ['a'],`)
})

test('defineProps/defineSlots in multi-variable declaration fix #6757 ', () => {
const { content } = compile(`
<script setup>
const a = 1,
props = defineProps(['item']),
slots = defineSlots(['a']);
</script>
`)
assertCode(content)
expect(content).toMatch(`const a = 1;`) // test correct removal
expect(content).toMatch(`props: ['item'],`)
expect(content).toMatch(`slots: ['a'],`)
})

test('defineProps/defineSlots in multi-variable declaration (full removal)', () => {
const { content } = compile(`
<script setup>
const props = defineProps(['item']),
slots = defineSlots(['a']);
</script>
`)
assertCode(content)
expect(content).toMatch(`props: ['item'],`)
expect(content).toMatch(`slots: ['a'],`)
})

test('defineExpose()', () => {
const { content } = compile(`
<script setup>
@@ -1136,7 +1195,7 @@ const emit = defineEmits(['a', 'b'])
`)
assertCode(content)
})

// #7111
test('withDefaults (static) w/ production mode', () => {
const { content } = compile(
@@ -1277,7 +1336,6 @@ const emit = defineEmits(['a', 'b'])
expect(content).toMatch(`emits: ["foo", "bar"]`)
})


test('defineEmits w/ type from normal script', () => {
const { content } = compile(`
<script lang="ts">
111 changes: 108 additions & 3 deletions packages/compiler-sfc/src/compileScript.ts
Original file line number Diff line number Diff line change
@@ -60,6 +60,7 @@ import { shouldTransform, transformAST } from '@vue/reactivity-transform'
// Special compiler macros
const DEFINE_PROPS = 'defineProps'
const DEFINE_EMITS = 'defineEmits'
const DEFINE_SLOTS = 'defineSlots'
const DEFINE_EXPOSE = 'defineExpose'
const WITH_DEFAULTS = 'withDefaults'

@@ -141,6 +142,9 @@ type PropsDeclType = FromNormalScript<TSTypeLiteral | TSInterfaceBody>
type EmitsDeclType = FromNormalScript<
TSFunctionType | TSTypeLiteral | TSInterfaceBody
>
type SlotsDeclType = FromNormalScript<
TSFunctionType | TSTypeLiteral | TSInterfaceBody
>

/**
* Compile `<script setup>`
@@ -286,6 +290,7 @@ export function compileScript(
let defaultExport: Node | undefined
let hasDefinePropsCall = false
let hasDefineEmitCall = false
let hasDefineSlotCall = false
let hasDefineExposeCall = false
let hasDefaultExportName = false
let hasDefaultExportRender = false
@@ -300,11 +305,16 @@ export function compileScript(
let emitsTypeDecl: EmitsDeclType | undefined
let emitsTypeDeclRaw: Node | undefined
let emitIdentifier: string | undefined
let slotsRuntimeDecl: Node | undefined
let slotsTypeDecl: EmitsDeclType | undefined
let slotsTypeDeclRaw: Node | undefined
let slotsIdentifier: string | undefined
let hasAwait = false
let hasInlinedSsrRenderFn = false
// props/emits declared via types
const typeDeclaredProps: Record<string, PropTypeData> = {}
const typeDeclaredEmits: Set<string> = new Set()
const typeDeclaredSlots: Set<string> = new Set()
// record declared types for runtime props type generation
const declaredTypes: Record<string, string[]> = {}
// props destructure data
@@ -590,6 +600,48 @@ export function compileScript(
return true
}

function processDefineSlots(node: Node, declId?: LVal): boolean {
if (!isCallOf(node, DEFINE_SLOTS)) {
return false
}
if (hasDefineSlotCall) {
error(`duplicate ${DEFINE_SLOTS}() call`, node)
}
hasDefineSlotCall = true
slotsRuntimeDecl = node.arguments[0]
if (node.typeParameters) {
if (slotsRuntimeDecl) {
error(
`${DEFINE_SLOTS}() cannot accept both type and non-type arguments ` +
`at the same time. Use one or the other.`,
node
)
}

slotsTypeDeclRaw = node.typeParameters.params[0]
slotsTypeDecl = resolveQualifiedType(
slotsTypeDeclRaw,
node => node.type === 'TSFunctionType' || node.type === 'TSTypeLiteral'
) as SlotsDeclType | undefined

if (!slotsTypeDecl) {
error(
`type argument passed to ${DEFINE_SLOTS}() must be a function type, ` +
`a literal type with call signatures, or a reference to the above types.`,
slotsTypeDeclRaw
)
}
}

if (declId) {
slotsIdentifier =
declId.type === 'Identifier'
? declId.name
: scriptSetup!.content.slice(declId.start!, declId.end!)
}

return true
}
function getAstBody(): Statement[] {
return scriptAst
? [...scriptSetupAst.body, ...scriptAst.body]
@@ -1194,6 +1246,7 @@ export function compileScript(
if (
processDefineProps(node.expression) ||
processDefineEmits(node.expression) ||
processDefineSlots(node.expression) ||
processWithDefaults(node.expression)
) {
s.remove(node.start! + startOffset, node.end! + startOffset)
@@ -1219,7 +1272,8 @@ export function compileScript(
processDefineProps(decl.init, decl.id, node.kind) ||
processWithDefaults(decl.init, decl.id, node.kind)
const isDefineEmits = processDefineEmits(decl.init, decl.id)
if (isDefineProps || isDefineEmits) {
const isDefineSlots = processDefineSlots(decl.init, decl.id)
if (isDefineProps || isDefineEmits || isDefineSlots) {
if (left === 1) {
s.remove(node.start! + startOffset, node.end! + startOffset)
} else {
@@ -1344,20 +1398,24 @@ export function compileScript(
}
}

// 4. extract runtime props/emits code from setup context type
// 4. extract runtime props/emits/slots code from setup context type
if (propsTypeDecl) {
extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes, isProd)
}
if (emitsTypeDecl) {
extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits)
}
if (slotsTypeDecl) {
extractRuntimeSlots(slotsTypeDecl, typeDeclaredSlots)
}

// 5. check useOptions args to make sure it doesn't reference setup scope
// variables
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
checkInvalidScopeReference(propsDestructureDecl, DEFINE_PROPS)
checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_EMITS)
checkInvalidScopeReference(slotsRuntimeDecl, DEFINE_SLOTS)

// 6. remove non-script content
if (script) {
@@ -1483,6 +1541,11 @@ export function compileScript(
emitIdentifier === `emit` ? `emit` : `emit: ${emitIdentifier}`
)
}
if (slotsIdentifier) {
destructureElements.push(
slotsIdentifier === `slots` ? `slots` : `slots: ${slotsIdentifier}`
)
}
if (destructureElements.length) {
args += `, { ${destructureElements.join(', ')} }`
if (emitsTypeDecl) {
@@ -1494,6 +1557,17 @@ export function compileScript(
emitsTypeDecl.end!
)}), expose: any, slots: any, attrs: any }`
}

// TODO review this part
if (slotsTypeDecl) {
const content = slotsTypeDecl.__fromNormalScript
? script!.content
: scriptSetup.content
args += `: { slots: (${content.slice(
slotsTypeDecl.start!,
slotsTypeDecl.end!
)}), expose: any, emit: any, attrs: any }`
}
Comment on lines +1561 to +1570
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this, this seems that might cause an issue when we have both slot and emit

}

// 10. generate return statement
@@ -1644,7 +1718,13 @@ export function compileScript(
} else if (emitsTypeDecl) {
runtimeOptions += genRuntimeEmits(typeDeclaredEmits)
}

if (slotsRuntimeDecl) {
runtimeOptions += `\n slots: ${scriptSetup.content
.slice(slotsRuntimeDecl.start!, slotsRuntimeDecl.end!)
.trim()},`
} else if (slotsTypeDecl) {
runtimeOptions += genRuntimeSlots(typeDeclaredSlots)
}
// <script setup> components are closed by default. If the user did not
// explicitly call `defineExpose`, call expose() with no args.
const exposeCall =
@@ -2015,6 +2095,23 @@ function extractRuntimeEmits(
}
}

function extractRuntimeSlots(
node: TSFunctionType | TSTypeLiteral | TSInterfaceBody,
slots: Set<string>
) {
if (node.type === 'TSTypeLiteral' || node.type === 'TSInterfaceBody') {
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
for (let t of members) {
if (t.type === 'TSCallSignatureDeclaration') {
extractEventNames(t.parameters[0], slots)
}
}
return
} else {
extractEventNames(node.parameters[0], slots)
}
}

function extractEventNames(
eventName: Identifier | RestElement,
emits: Set<string>
@@ -2054,6 +2151,14 @@ function genRuntimeEmits(emits: Set<string>) {
: ``
}

function genRuntimeSlots(slots: Set<string>) {
return slots.size
? `\n slots: [${Array.from(slots)
.map(p => JSON.stringify(p))
.join(', ')}],`
: ``
}

function isCallOf(
node: Node | null | undefined,
test: string | ((id: string) => boolean) | null | undefined
107 changes: 105 additions & 2 deletions packages/dts-test/defineComponent.test-d.tsx
Original file line number Diff line number Diff line change
@@ -510,9 +510,9 @@ describe('with mixins', () => {
mixins: [MixinA],
data() {
//@ts-expect-error computed are not available on data()
expectError<number>(this.dC1)
expectType<number>(this.dC1)
//@ts-expect-error computed are not available on data()
expectError<string>(this.dC2)
expectType<string>(this.dC2)

return {
d: 4
@@ -1250,6 +1250,108 @@ describe('prop starting with `on*` is broken', () => {
})
})

describe('typed slots', () => {
describe('Object declaration', () => {
const Comp = defineComponent({
slots: {
test: null,
item: Object as () => { item: { value: number }; i: number }
},

setup(_, { slots }) {
slots.test!()
slots.item!({
i: 22,
item: {
value: 22
}
})
// @ts-expect-error missing item prop
expectError(slots.item!({ i: 22 }))
}
})

h(
Comp,
{},
{
// @ts-expect-error no argument expected
test(x) {},
item(s) {
expectType<number>(s.i)
expectType<{ value: number }>(s.item)
}
}
)

h(Comp, {}, {})
})

describe('Type API', () => {
const Comp = defineComponent({
slots: {} as {
test: null
item: { item: { value: number }; i: number }
},

setup(_, { slots }) {
slots.test!()
slots.item!({
i: 22,
item: {
value: 22
}
})
// @ts-expect-error missing item prop
slots.item!({ i: 22 })
}
})

h(
Comp,
{},
{
item() {},
test() {}
}
)

h(
Comp,
{},
{
// @ts-expect-error no argument expected
test(x) {},
item(s) {
expectType<number>(s.i)
expectType<{ value: number }>(s.item)
}
}
)
h(Comp, {}, {})
})

describe('string Array', () => {
const Comp = defineComponent({
slots: ['test', 'item'] as const,

setup(_, { slots }) {
slots.test!()
slots.item!({
i: 22,
item: {
value: 22
}
})
// @ts-expect-error not a valid slot
slots.other!({ i: 22 })
}
})

h(Comp, {}, {})
})
})

// check if defineComponent can be exported
export default {
// function components
@@ -1292,6 +1394,7 @@ declare const MyButton: DefineComponent<
ComponentOptionsMixin,
EmitsOptions,
string,
{},
VNodeProps & AllowedComponentProps & ComponentCustomProps,
Readonly<ExtractPropTypes<{}>>,
{}
4 changes: 4 additions & 0 deletions packages/runtime-core/__tests__/apiSetupHelpers.spec.ts
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ import {
} from '@vue/runtime-test'
import {
defineEmits,
defineSlots,
defineProps,
defineExpose,
withDefaults,
@@ -35,6 +36,9 @@ describe('SFC <script setup> helpers', () => {
defineEmits()
expect(`defineEmits() is a compiler-hint`).toHaveBeenWarned()

defineSlots()
expect(`defineSlots() is a compiler-hint`).toHaveBeenWarned()

defineExpose()
expect(`defineExpose() is a compiler-hint`).toHaveBeenWarned()

5 changes: 5 additions & 0 deletions packages/runtime-core/__tests__/components/KeepAlive.spec.ts
Original file line number Diff line number Diff line change
@@ -740,6 +740,11 @@ describe('KeepAlive', () => {
const spyUnmounted = vi.fn()

const RouterView = defineComponent({
slots: null as unknown as {
default: {
Component: ComponentPublicInstance
}
},
Comment on lines +743 to +747
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strict behaviour by default, this behaviour is a breaking change, I can make it less strict if needed

setup(_, { slots }) {
const Component = inject<Ref<ComponentPublicInstance>>('component')
const refView = ref()
32 changes: 27 additions & 5 deletions packages/runtime-core/src/apiDefineComponent.ts
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ import {
CreateComponentPublicInstance,
ComponentPublicInstanceConstructor
} from './componentPublicInstance'
import { Slots } from './componentSlots'

export type PublicProps = VNodeProps &
AllowedComponentProps &
@@ -41,6 +42,7 @@ export type DefineComponent<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = {},
EE extends string = string,
S = {},
PP = PublicProps,
Props = Readonly<
PropsOrPropOptions extends ComponentPropsOptions
@@ -59,6 +61,7 @@ export type DefineComponent<
Mixin,
Extends,
E,
S,
PP & Props,
Defaults,
true
@@ -75,6 +78,7 @@ export type DefineComponent<
Extends,
E,
EE,
S,
Defaults
> &
PP
@@ -86,12 +90,23 @@ export type DefineComponent<

// overload 1: direct setup function
// (uses user defined props interface)
export function defineComponent<Props, RawBindings = object>(
export function defineComponent<Props, RawBindings = object, S = {}>(
setup: (
props: Readonly<Props>,
ctx: SetupContext
ctx: SetupContext<EmitsOptions, Slots<S>>
) => RawBindings | RenderFunction
): DefineComponent<Props, RawBindings>
): DefineComponent<
Props,
RawBindings,
{},
ComputedOptions,
MethodOptions,
ComponentOptionsMixin,
ComponentOptionsMixin,
{},
string,
S
>

// overload 2: object format with no props
// (uses user defined props interface)
@@ -106,6 +121,7 @@ export function defineComponent<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = {},
EE extends string = string,
S = {},
I extends ComponentInjectOptions = {},
II extends string = string
>(
@@ -119,10 +135,11 @@ export function defineComponent<
Extends,
E,
EE,
S,
I,
II
>
): DefineComponent<Props, RawBindings, D, C, M, Mixin, Extends, E, EE>
): DefineComponent<Props, RawBindings, D, C, M, Mixin, Extends, E, EE, S>

// overload 3: object format with array props declaration
// props inferred as { [key in PropNames]?: any }
@@ -137,6 +154,7 @@ export function defineComponent<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = {},
EE extends string = string,
S = {},
I extends ComponentInjectOptions = {},
II extends string = string
>(
@@ -150,6 +168,7 @@ export function defineComponent<
Extends,
E,
EE,
S,
I,
II
>
@@ -162,7 +181,8 @@ export function defineComponent<
Mixin,
Extends,
E,
EE
EE,
S
>

// overload 4: object format with object props declaration
@@ -179,6 +199,7 @@ export function defineComponent<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = {},
EE extends string = string,
S = {},
I extends ComponentInjectOptions = {},
II extends string = string
>(
@@ -192,6 +213,7 @@ export function defineComponent<
Extends,
E,
EE,
S,
I,
II
>
34 changes: 34 additions & 0 deletions packages/runtime-core/src/apiSetupHelpers.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import {
unsetCurrentInstance
} from './component'
import { EmitFn, EmitsOptions } from './componentEmits'
import { Slots } from './componentSlots'
import {
ComponentPropsOptions,
ComponentObjectPropsOptions,
@@ -115,6 +116,39 @@ export function defineEmits() {
return null as any
}

/**
* Vue `<script setup>` compiler macro for declaring a component's available
* slots. The expected argument is the same as the component `slots` option.
*
* Example runtime declaration:
* ```js
* const slots = defineSlots(['item'])
* ```
*
* Example type-based declaration:
* ```ts
* const slots = defineSlots<{
* item: { value: string }
* }>()
*
* <slot name='item' value="test"></slot>
* ```
*
* This is only usable inside `<script setup>`, is compiled away in the
* output and should **not** be actually called at runtime.
*/
// overload 1: runtime slots w/ array
export function defineSlots<SS extends string = string>(slots: SS[]): Slots<SS>
export function defineSlots<S>(slots: S): Slots<S>
export function defineSlots<S>(): Slots<S>
// implementation
export function defineSlots() {
if (__DEV__) {
warnRuntimeUsage(`defineSlots`)
}
return null as any
}

/**
* Vue `<script setup>` compiler macro for declaring a component's exposed
* instance properties when it is accessed by a parent component via template
4 changes: 3 additions & 1 deletion packages/runtime-core/src/compat/componentFunctional.ts
Original file line number Diff line number Diff line change
@@ -13,7 +13,9 @@ const normalizedFunctionalComponentMap = new Map<
FunctionalComponent
>()

export const legacySlotProxyHandlers: ProxyHandler<InternalSlots> = {
export const legacySlotProxyHandlers: ProxyHandler<
InternalSlots<Record<string, null>>
> = {
get(target, key: string) {
const slot = target[key]
return slot && slot()
13 changes: 8 additions & 5 deletions packages/runtime-core/src/component.ts
Original file line number Diff line number Diff line change
@@ -116,10 +116,13 @@ export interface ComponentInternalOptions {
__name?: string
}

export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
extends ComponentInternalOptions {
export interface FunctionalComponent<
P = {},
E extends EmitsOptions = {},
S extends Slots = Slots
> extends ComponentInternalOptions {
// use of any here is intentional so it can be a valid JSX Element constructor
(props: P, ctx: Omit<SetupContext<E>, 'expose'>): any
(props: P, ctx: Omit<SetupContext<E, S>, 'expose'>): any
props?: ComponentPropsOptions<P>
emits?: E | (keyof E)[]
inheritAttrs?: boolean
@@ -167,10 +170,10 @@ export type { ComponentOptions }
type LifecycleHook<TFn = Function> = TFn[] | null

// use `E extends any` to force evaluating type to fix #2362
export type SetupContext<E = EmitsOptions> = E extends any
export type SetupContext<E = EmitsOptions, S extends Slots = Slots> = E extends any
? {
attrs: Data
slots: Slots
slots: S
emit: EmitFn<E>
expose: (exposed?: Record<string, any>) => void
}
15 changes: 14 additions & 1 deletion packages/runtime-core/src/componentOptions.ts
Original file line number Diff line number Diff line change
@@ -71,6 +71,7 @@ import {
softAssertCompatEnabled
} from './compat/compatConfig'
import { OptionMergeFunction } from './apiCreateApp'
import { Slots } from './componentSlots'
import { LifecycleHooks } from './enums'

/**
@@ -118,6 +119,7 @@ export interface ComponentOptionsBase<
Extends extends ComponentOptionsMixin,
E extends EmitsOptions,
EE extends string = string,
S = {},
Defaults = {},
I extends ComponentInjectOptions = {},
II extends string = string
@@ -133,7 +135,7 @@ export interface ComponentOptionsBase<
UnionToIntersection<ExtractOptionProp<Extends>>
>
>,
ctx: SetupContext<E>
ctx: SetupContext<E, Slots<S>>
) => Promise<RawBindings> | RawBindings | RenderFunction | void
name?: string
template?: string | object // can be a direct DOM node
@@ -154,6 +156,8 @@ export interface ComponentOptionsBase<
// Runtime compiler only -----------------------------------------------------
compilerOptions?: RuntimeCompilerOptions

slots?: S & ThisType<void>

// Internal ------------------------------------------------------------------

/**
@@ -227,6 +231,7 @@ export type ComponentOptionsWithoutProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
S = {},
I extends ComponentInjectOptions = {},
II extends string = string,
PE = Props & EmitsToProps<E>
@@ -240,6 +245,7 @@ export type ComponentOptionsWithoutProps<
Extends,
E,
EE,
S,
{},
I,
II
@@ -255,6 +261,7 @@ export type ComponentOptionsWithoutProps<
Mixin,
Extends,
E,
S,
PE,
{},
false,
@@ -272,6 +279,7 @@ export type ComponentOptionsWithArrayProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
S = {},
I extends ComponentInjectOptions = {},
II extends string = string,
Props = Readonly<{ [key in PropNames]?: any }> & EmitsToProps<E>
@@ -285,6 +293,7 @@ export type ComponentOptionsWithArrayProps<
Extends,
E,
EE,
S,
{},
I,
II
@@ -300,6 +309,7 @@ export type ComponentOptionsWithArrayProps<
Mixin,
Extends,
E,
S,
Props,
{},
false,
@@ -317,6 +327,7 @@ export type ComponentOptionsWithObjectProps<
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
S = {},
I extends ComponentInjectOptions = {},
II extends string = string,
Props = Readonly<ExtractPropTypes<PropsOptions>> & EmitsToProps<E>,
@@ -331,6 +342,7 @@ export type ComponentOptionsWithObjectProps<
Extends,
E,
EE,
S,
Defaults,
I,
II
@@ -346,6 +358,7 @@ export type ComponentOptionsWithObjectProps<
Mixin,
Extends,
E,
S,
Props,
Defaults,
false,
21 changes: 18 additions & 3 deletions packages/runtime-core/src/componentPublicInstance.ts
Original file line number Diff line number Diff line change
@@ -88,6 +88,7 @@ type MixinToOptionTypes<T> = T extends ComponentOptionsBase<
infer Extends,
any,
any,
any,
infer Defaults
>
? OptionTypesType<P & {}, B & {}, D & {}, C & {}, M & {}, Defaults & {}> &
@@ -140,6 +141,7 @@ export type CreateComponentPublicInstance<
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = {},
S = {},
PublicProps = P,
Defaults = {},
MakeDefaultsOptional extends boolean = false,
@@ -161,10 +163,11 @@ export type CreateComponentPublicInstance<
PublicC,
PublicM,
E,
S,
PublicProps,
PublicDefaults,
MakeDefaultsOptional,
ComponentOptionsBase<P, B, D, C, M, Mixin, Extends, E, string, Defaults>,
ComponentOptionsBase<P, B, D, C, M, Mixin, Extends, E, string, S, Defaults>,
I
>

@@ -177,10 +180,22 @@ export type ComponentPublicInstance<
C extends ComputedOptions = {},
M extends MethodOptions = {},
E extends EmitsOptions = {},
S = {},
PublicProps = P,
Defaults = {},
MakeDefaultsOptional extends boolean = false,
Options = ComponentOptionsBase<any, any, any, any, any, any, any, any, any>,
Options = ComponentOptionsBase<
any,
any,
any,
any,
any,
any,
any,
any,
any,
any
>,
I extends ComponentInjectOptions = {}
> = {
$: ComponentInternalInstance
@@ -190,7 +205,7 @@ export type ComponentPublicInstance<
: P & PublicProps
$attrs: Data
$refs: Data
$slots: Slots
$slots: Slots<S>
$root: ComponentPublicInstance | null
$parent: ComponentPublicInstance | null
$emit: EmitFn<E>
26 changes: 21 additions & 5 deletions packages/runtime-core/src/componentSlots.ts
Original file line number Diff line number Diff line change
@@ -24,14 +24,27 @@ import { toRaw } from '@vue/reactivity'

export type Slot = (...args: any[]) => VNode[]

export type InternalSlots = {
[name: string]: Slot | undefined
export type SlotTyped<T> = T extends null ? () => VNode[] : (arg: T) => VNode[]

export type InternalSlots<T = any> = {
[K in keyof T]?: T[K] extends () => infer R ? SlotTyped<R> : SlotTyped<T[K]>
}

export type Slots = Readonly<InternalSlots>
export type SlotsObject<T = any> = InternalSlots<T>
export type SlotArray<V = PropertyKey> = [V] extends [PropertyKey]
? Record<V, Slot>
: Record<string, Slot>

export type RawSlots = {
[name: string]: unknown
export type Slots<T = any> = RenderSlot &
(unknown extends T
? Readonly<Partial<Record<string, Slot>>>
: T extends Array<infer V>
? Readonly<SlotArray<V>>
: T extends ReadonlyArray<infer V>
? Readonly<SlotArray<V>>
: Readonly<SlotsObject<T>>)

export type RenderSlot = {
// manual render fn hint to skip forced children updates
$stable?: boolean
/**
@@ -50,6 +63,9 @@ export type RawSlots = {
*/
_?: SlotFlags
}
export type RawSlots = {
[name: string]: unknown
} & RenderSlot

const isInternalKey = (key: string) => key[0] === '_' || key === '$stable'

19 changes: 14 additions & 5 deletions packages/runtime-core/src/h.ts
Original file line number Diff line number Diff line change
@@ -68,11 +68,14 @@ type RawChildren =
| (() => any)

// fake constructor type returned from `defineComponent`
interface Constructor<P = any> {
interface Constructor<P = any, S = any> {
__isFragment?: never
__isTeleport?: never
__isSuspense?: never
new (...args: any[]): { $props: P }
new (...args: any[]): {
$props: P
$slots?: S
}
}

// The following is a series of overloads for providing props validation of
@@ -156,10 +159,16 @@ export function h<P>(

// fake constructor type returned by `defineComponent` or class component
export function h(type: Constructor, children?: RawChildren): VNode
export function h<P>(
type: Constructor<P>,
export function h<P, S>(
type: Constructor<P, S>,
props?: (RawProps & P) | ({} extends P ? null : never),
children?: RawChildren | RawSlots
children?: (RawChildren & S) | ({} extends S ? RawSlots : Partial<S>)
): VNode
// for the string array slot declaration
export function h<P, S>(
type: Constructor<P, S>,
props?: (RawProps & P) | ({} extends P ? null : never),
children?: RawChildren & Partial<S>
): VNode

// fake constructor type returned by `defineComponent`
1 change: 1 addition & 0 deletions packages/runtime-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -67,6 +67,7 @@ export {
// macros runtime, for typing and warnings only
defineProps,
defineEmits,
defineSlots,
defineExpose,
withDefaults,
// internal
2 changes: 2 additions & 0 deletions packages/runtime-core/types/scriptSetupHelpers.d.ts
Original file line number Diff line number Diff line change
@@ -2,12 +2,14 @@
// build.
type _defineProps = typeof defineProps
type _defineEmits = typeof defineEmits
type _defineSlots = typeof defineSlots
type _defineExpose = typeof defineExpose
type _withDefaults = typeof withDefaults

declare global {
const defineProps: _defineProps
const defineEmits: _defineEmits
const defineSlots: _defineSlots
const defineExpose: _defineExpose
const withDefaults: _withDefaults
}
3 changes: 3 additions & 0 deletions packages/runtime-dom/src/apiCustomElement.ts
Original file line number Diff line number Diff line change
@@ -64,6 +64,7 @@ export function defineCustomElement<
Extends,
E,
EE,
{},
I,
II
> & { styles?: string[] }
@@ -93,6 +94,7 @@ export function defineCustomElement<
Extends,
E,
EE,
{},
I,
II
> & { styles?: string[] }
@@ -122,6 +124,7 @@ export function defineCustomElement<
Extends,
E,
EE,
{},
I,
II
> & { styles?: string[] }