From 787ceef607b8af394462eba599da2dd8f1967c06 Mon Sep 17 00:00:00 2001 From: ubugeeei Date: Tue, 15 Sep 2026 15:45:52 +0900 Subject: [PATCH 1/3] docs: document typed slot children and Renders --- .vitepress/config.ts | 2 +- src/api/sfc-script-setup.md | 105 +++++++++++++++++++++++++++++++++++- 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 0abc1fec9a..21b04f2e48 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -813,7 +813,7 @@ Vue.js - The Progressive JavaScript Framework ## Table of Contents {toc}` - }) as Plugin, + }) as Plugin[], groupIconVitePlugin({ customIcon: { cypress: 'vscode-icons:file-type-cypress', diff --git a/src/api/sfc-script-setup.md b/src/api/sfc-script-setup.md index 33649b7d3d..dc34004a99 100644 --- a/src/api/sfc-script-setup.md +++ b/src/api/sfc-script-setup.md @@ -409,7 +409,7 @@ defineOptions({ This macro can be used to provide type hints to IDEs for slot name and props type checking. -`defineSlots()` only accepts a type parameter and no runtime arguments. The type parameter should be a type literal where the property key is the slot name, and the value type is the slot function. The first argument of the function is the props the slot expects to receive, and its type will be used for slot props in the template. The return type is currently ignored and can be `any`, but we may leverage it for slot content checking in the future. +`defineSlots()` only accepts a type parameter and no runtime arguments. The type parameter should be a type literal where the property key is the slot name, and the value type is the slot function. The first argument of the function is the props the slot expects to receive, and its type will be used for slot props in the template. The return type is ignored by default and can be `any`. Experimental tooling can use it to [check slot children](#typed-slot-children). It also returns the `slots` object, which is equivalent to the `slots` object exposed on the setup context or returned by `useSlots()`. @@ -421,6 +421,109 @@ const slots = defineSlots<{ ``` +### Typed slot children (experimental) {#typed-slot-children} + +::: warning Draft proposal +This section describes the experimental implementation of [RFC 734](https://github.com/vuejs/rfcs/pull/734), targeting a future minor release of Vue Language Tools. It requires a build containing that implementation; it is not available in current stable tooling. The option and helper types may change before the RFC is accepted. +::: + +Slot return types can describe which children a component accepts. Enable the experiment explicitly in `tsconfig.json`: + +```json +{ + "compilerOptions": { "strict": true }, + "vueCompilerOptions": { "strictSlotChildren": true } +} +``` + +`strictTemplates` does not enable this experiment. Both the Vue language extension and `vue-tsc` must use a version that supports it. + +For example, a generic `Tabs.vue` can accept only `TabItem` children with matching value types: + +```vue + + + +``` + +Here `TabItem.vue` declares `generic="T"` and `defineProps<{ value: T }>()`. A consumer can write: + +```vue + + + + + + + + + + +``` + +The slot type can also be an imported interface, a re-exported type alias, or a generic type. TypeScript resolves these types, including component generic arguments; they do not need to be written inline. + +#### Child types and cardinality + +The return type describes the rendered children, with template fragments flattened: + +| Return type | Accepted content | +| --- | --- | +| `HTMLInputElement` | One input element | +| `HTMLInputElement[]` | Zero or more input elements | +| `readonly [HTMLInputElement, HTMLButtonElement]` | An input followed by a button | +| `Renders>` | One compatible `TabItem` with a numeric value | +| `Renders>[]` | Zero or more compatible items | +| `string` or `Text` | One rendered text node, including interpolation | +| `HTMLInputElement \| undefined` | An input or no rendered children | +| `[]` | No rendered children | +| `VNode[]` | Any number of VNodes, including text VNodes | +| `any` or `unknown` | Unrestricted content | + +Comments and formatting whitespace do not count as children. Adjacent text and interpolations form one text node. A native element's descendants belong to that element, not to the surrounding slot. + +Each `v-if` branch must satisfy the constraint, including the empty branch when there is no `v-else`. A `v-for` can render zero or many children, so it cannot satisfy a required single child or a fixed-length tuple. Named slots are checked independently. An optional slot may be omitted; if explicitly provided, its content must satisfy its return type. + +#### Component unions and bound props + +Use `Renders` from `vue-component-type-helpers` to describe component render types. The optional second argument constrains the props actually passed to the child: + +```ts +import type { Renders } from 'vue-component-type-helpers' +import type TabItem from './TabItem.vue' +import type TabSeparator from './TabSeparator.vue' + +defineSlots<{ + default(): Renders | typeof TabSeparator>[] + selected(): Renders, { selected: true }> +}>() +``` + +`Renders` also accepts the spelling `Renders | Renders`. Each component remains associated with its own props. `(Renders | Renders)[]` allows mixed children; `Renders[] | Renders[]` requires all children to belong to one alternative. Unions of tuples preserve their order and length constraints. + +An optional prop declaration alone does not satisfy a required bound prop. `Renders` accepts any component receiving a numeric `value` prop. The helper describes types for tooling; slot functions still return VNodes at runtime. + +#### Relationship to Flow's `renders` + +This feature serves the same composition-contract use case as [Flow's render types](https://flow.org/en/docs/react/render-types/): a design-system component can restrict which components may appear inside it. `Renders` corresponds conceptually to `renders Comp`, `Renders | undefined` to `renders? Comp`, and `Renders[]` to `renders* Comp`. TypeScript keeps its existing generic syntax; no new keyword is needed. + +The tooling also infers the roots of SFC wrappers. A wrapper rendering `TabItem` can be accepted where a `TabItem` is expected, including generic wrappers and wrappers with multiple roots. A wrapper containing `
` renders a `div` at its root, so it does not satisfy a `TabItem` constraint. + +SFCs checked with this option carry a distinct component identity in their generated types. This prevents unrelated SFCs with identical props from satisfying each other's render constraints. Wrappers retain that identity across imports and declaration output. Components without this metadata and native DOM interfaces follow TypeScript's structural assignability rules. + +For a wrapper forwarding a slot, the checker follows the children supplied by its caller and uses the fallback when the slot is absent or empty. Every possible conditional branch must satisfy the constraint. Wrapper cycles that cannot establish the requested render type are rejected; finite wrapper chains are not limited by a fixed wrapper-depth setting. As with other TypeScript types, sufficiently complex expressions can reach TypeScript's own instantiation limits. + +Root inference requires an SFC template checked with this option. Arbitrary render functions do not expose their rendered roots through their usual `VNode` return type. This feature checks template composition at development time and adds no runtime validation. + + ## `useSlots()` & `useAttrs()` {#useslots-useattrs} Usage of `slots` and `attrs` inside ` - + ``` Here `TabItem.vue` declares `generic="T"` and `defineProps<{ value: T }>()`. A consumer can write: @@ -490,7 +492,9 @@ The return type describes the rendered children, with template fragments flatten Comments and formatting whitespace do not count as children. Adjacent text and interpolations form one text node. A native element's descendants belong to that element, not to the surrounding slot. -Each `v-if` branch must satisfy the constraint, including the empty branch when there is no `v-else`. A `v-for` can render zero or many children, so it cannot satisfy a required single child or a fixed-length tuple. Named slots are checked independently. An optional slot may be omitted; if explicitly provided, its content must satisfy its return type. +Each `v-if` branch must satisfy the constraint, including the empty branch when there is no `v-else`. A `v-for` can render zero or many children, so it cannot satisfy a required single child or a fixed-length tuple. Named slots are checked independently. An optional slot may be omitted; if explicitly provided, its content must satisfy its return type. Required slots whose return types exclude empty content must be provided on every branch. + +For dynamic slot names and dynamic components, the content must satisfy every possible slot contract. A dynamic name supplies one of its possible names, rather than all of them. Conditional named slots retain which names are present together on each branch. #### Component unions and bound props @@ -513,7 +517,7 @@ An optional prop declaration alone does not satisfy a required bound prop. `Rend #### Relationship to Flow's `renders` -This feature serves the same composition-contract use case as [Flow's render types](https://flow.org/en/docs/react/render-types/): a design-system component can restrict which components may appear inside it. `Renders` corresponds conceptually to `renders Comp`, `Renders | undefined` to `renders? Comp`, and `Renders[]` to `renders* Comp`. TypeScript keeps its existing generic syntax; no new keyword is needed. +Like [Flow's render types](https://flow.org/en/docs/react/render-types/), this lets a component specify which components it accepts as children. `Renders` corresponds conceptually to `renders Comp`, `Renders | undefined` to `renders? Comp`, and `Renders[]` to `renders* Comp`. TypeScript keeps its existing generic syntax; no new keyword is needed. The tooling also infers the roots of SFC wrappers. A wrapper rendering `TabItem` can be accepted where a `TabItem` is expected, including generic wrappers and wrappers with multiple roots. A wrapper containing `
` renders a `div` at its root, so it does not satisfy a `TabItem` constraint. From 2d50dfc0cc48c223034f61f7edf9251dc30f7440 Mon Sep 17 00:00:00 2001 From: ubugeeei Date: Tue, 15 Sep 2026 17:17:21 +0900 Subject: [PATCH 3/3] docs: align slot section headings with the writing guide --- src/api/sfc-script-setup.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/api/sfc-script-setup.md b/src/api/sfc-script-setup.md index 3c371df78b..19ebf1c1ec 100644 --- a/src/api/sfc-script-setup.md +++ b/src/api/sfc-script-setup.md @@ -421,7 +421,7 @@ const slots = defineSlots<{ ``` -### Typed slot children (experimental) {#typed-slot-children} +### Restricting Slot Children (Experimental) {#typed-slot-children} ::: warning Draft proposal This section describes the experimental implementation of [RFC 734](https://github.com/vuejs/rfcs/pull/734), targeting a future minor release of Vue Language Tools. It requires a build containing that implementation; it is not available in current stable tooling. The option and helper types may change before the RFC is accepted. @@ -473,7 +473,7 @@ Here `TabItem.vue` declares `generic="T"` and `defineProps<{ value: T }>()`. A c The slot type can also be an imported interface, a re-exported type alias, or a generic type. TypeScript resolves these types, including component generic arguments; they do not need to be written inline. -#### Child types and cardinality +#### Child Types and Counts The return type describes the rendered children, with template fragments flattened: @@ -496,7 +496,7 @@ Each `v-if` branch must satisfy the constraint, including the empty branch when For dynamic slot names and dynamic components, the content must satisfy every possible slot contract. A dynamic name supplies one of its possible names, rather than all of them. Conditional named slots retain which names are present together on each branch. -#### Component unions and bound props +#### Component Unions and Bound Props Use `Renders` from `vue-component-type-helpers` to describe component render types. The optional second argument constrains the props actually passed to the child: @@ -527,7 +527,6 @@ For a wrapper forwarding a slot, the checker follows the children supplied by it Root inference requires an SFC template checked with this option. Arbitrary render functions do not expose their rendered roots through their usual `VNode` return type. This feature checks template composition at development time and adds no runtime validation. - ## `useSlots()` & `useAttrs()` {#useslots-useattrs} Usage of `slots` and `attrs` inside `