Vue RFC Experiments
Vize can try four Vue RFCs before they land in Vue. Each one is off until you turn on its own flag, and turning on one never enables another. The RFC on GitHub is the design source; this page shows what Vize implements today and where it stops. To try one, start with Enable an experiment.
Warning
These features follow open proposals. Syntax and behavior can change with the RFC, and code that uses them only compiles with Vize.
Enable an experiment
| RFC | What it adds | Flag | Compiler field | Works in |
|---|---|---|---|---|
| #823 | v-match / v-when pattern matching in templates |
patternedTemplate |
experimentalPatternedTemplate |
DOM, SSR, Vapor, vize check, LSP diagnostics |
| #831 | // comments inside an opening tag |
inTagComment |
experimentalInTagComments |
Parser and every compile target; no runtime output |
| #833 | <Self> for the current component |
selfComponent |
experimentalSelfComponent |
DOM, SSR, Vapor |
| #734 | Type checks for the children passed to a slot | strictSlotChildren |
experimentalStrictSlotChildren |
vize check and the language server only |
Turn flags on under vue.experimentals in vite.config.ts:
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({}).withVue({
experimentals: {
inTagComment: true,
patternedTemplate: true,
selfComponent: true,
strictSlotChildren: true,
},
});
With a flag off, Vize treats the syntax as it would without the RFC: v-match, v-when, and
v-case report that the opt-in is required, // in a tag is invalid syntax, <Self> is an
ordinary component tag, and no extra slot child types are generated.
Integrations that call the compiler directly pass the resolved booleans instead:
compile(templateSource, { experimentalPatternedTemplate: true });
compileVapor(templateSource, { experimentalSelfComponent: true, componentName: "TreeNode" });
parseTemplate(templateSource, { experimentalInTagComments: true });
compileSfc(sfcSource, { filename: "TreeNode.vue", experimentalStrictSlotChildren: true });
These direct fields on compile, compileVapor, parseTemplate, and compileSfc* take final
booleans only. They do not understand aliases such as pattenedTemplate or intagComment, or {}
switch objects. A value passed directly to the Vite plugin still wins over shared config, including an
explicit false. Experimentals covers precedence, and the
Experimentals Reference lists every entry point.
Patterned Templates
patternedTemplate adds the long-form v-match / v-when syntax from RFC #823. Put v-match on a
container and v-when on each direct child. Vize evaluates the subject once, tries the branches in
order, and renders only the first one that matches.
<script setup lang="ts">
type Result =
| { status: "success"; data: { title: string; published: boolean } }
| { status: "error"; error: { message: string; retriable: boolean } }
| { status: "loading" }
| { status: "empty" };
const result = ref<Result>({ status: "loading" });
</script>
<template v-match="result">
<ArticleView
v-when="{ status: 'success', data: const article } if (article.published)"
:article="article"
/>
<RetryBanner
v-when="{ status: 'error', error: const error } if (error.retriable)"
:error="error"
/>
<p v-when="{ status: 'empty' } | { status: 'loading' }">Waiting for content.</p>
<ErrorBanner v-when="{ status: 'error', error: const error }" :error="error" />
<p v-when="_">Unpublished article.</p>
</template>
How it behaves:
- Bindings declared with
constare branch-local. The branch element, its attributes, its guard, and its children can read them; sibling branches cannot. - Each property is read once per attempted branch, and guards see the same values as the rendered branch. A guard only runs after its pattern matched.
v-matchrenders no extra fragment or list marker in DOM, SSR, or Vapor. A single-root match keeps attribute fallthrough, works insideTransitionandKeepAlive, and hydrates node for node.- A branch's
:keyis evaluated with that branch's bindings. - An outer
<template v-match>works in SFCs too. Externalsrctemplates and preprocessors are rejected.
| Pattern | Example | Matches when |
|---|---|---|
| Literal | v-when="'ready'", v-when="404" |
strictly equal |
| Value | v-when="Status.Ready" |
equal to an identifier or member expression (SameValueZero, so NaN matches) |
| Wildcard | v-when="_" |
always; binds nothing |
| Const binding | v-when="const value" |
always; binds value |
| Object | v-when="{ kind: 'ok', value: const data }" |
the listed properties match; extra properties are allowed |
| Object shorthand | v-when="{ kind: 'ok', const data }" |
same as { data: const data } |
| Object rest | v-when="{ kind: 'error', ...const payload }" |
binds the remaining own enumerable properties |
| Array / tuple | v-when="[const first, ...const rest]" |
the value is an array (Array.isArray); rest allows more items |
| Or | v-when="'idle' | 'loading'" |
any alternative matches, tried left to right |
| As binding | v-when="{ kind: 'ok' } as whole" |
the pattern matches; binds the whole value as whole |
| Guard | v-when="{ error: const e } if (e.retriable)" |
the pattern matches and the guard is truthy |
Patterned Diagnostics
Vize reports misuse instead of passing unknown directives through. Without the flag, it says the opt-in is required:
<template v-match="status">
<p v-when="'ready'">Ready</p>
</template>
v-when must be a direct child of the v-match container:
<template v-match="status">
<section>
<p v-when="'ready'">Ready</p>
</section>
</template>
An unguarded fallback must be unique and last:
<template v-match="status">
<p v-when="_">Other</p>
<p v-when="'ready'">Ready</p>
<p v-when="_">Again</p>
</template>
v-match must have at least one direct branch, and v-when cannot use directive arguments or
modifiers:
<template v-match="status"></template>
<template v-match="status">
<p v-when:ready="'ready'">Ready</p>
<p v-when.once="'ready'">Ready again</p>
</template>
Also rejected:
letandvarbindings are rejected;constis the only binding declaration.v-whenon the same element asv-if,v-else-if,v-else,v-for, orv-match.- The old
as const namespelling; writepattern as name. v-caseandv-case.defaultare kept only as aliases for older Vize experiments; usev-whenand_in new templates.
Patterned Type Boundary
With the flag on, vize check understands patterns:
- Branch bindings and the subject are narrowed to the matched shape.
- Missing coverage is an error anchored to the authored subject and fails
vize check; add an unguardedv-when="_"when coverage cannot be proven. - Unreachable branches are warnings and do not fail the check.
- Guards never prove coverage. Optional properties keep
undefinedin the remaining cases. - Finite object and tuple unions, readonly arrays, and rest bindings are supported. Open primitives,
any,unknown, and union-valued value patterns stay conservative. - Binding inside an or-pattern alternative is also deferred; split cases that need bindings into
separate branches. The RFC shorthands
?=,|=, and~=are not supported.
| Tool | Support today |
|---|---|
| Compiler (DOM, SSR, Vapor) | Full RFC syntax |
vize check |
Narrowing, coverage errors, unreachable-branch warnings |
| Language server | Structural and coverage diagnostics, hover and go to definition for root-pattern bindings; restart the server after changing the flag |
| Playground | Croquis and Canon checkboxes; full type diagnostics use the Monaco TypeScript worker |
| VS Code grammar | Highlights v-when patterns without the flag |
| Not yet | Content Mapper routing, and full rename and completion for guards, nested closures, and or-pattern bindings (#6176) |
The full upstream type-tooling acceptance contract for narrowing and exhaustiveness also includes
editor navigation and completion, which are still in progress. Virtual-TS hosts other than Vize must
register virtualTsHelpers once as an ambient declaration file.
In-Tag Comments
inTagComment allows // comments inside an opening tag's attribute list (RFC #831). They exist only
at compile time: Vize keeps their text for tools and emits no runtime code.
<template>
<LegacySelect
:options="options"
// @vue-expect-error legacy API accepts string IDs
:selected-id="selectedId"
/>
</template>
- Put them after the tag name, between complete attributes, or after the last attribute.
- Put
>or/>on the next line after a trailing comment://consumes the closing delimiter when>or/>stays on the same line. - They are not valid inside a tag name, attribute name, directive name or argument, modifier, or
attribute value.
//inside an attribute value stays text:
<template>
<a href="https://example.test/a//b">Link</a>
</template>
For tools, the comment is stored in root.comments with CommentKind::InTag. It is not an element
prop or a <!-- --> child, and comments: false does not remove it.
Raw in-DOM templates are parsed by the browser first, so do not rely on in-tag comments there.
Self Component
selfComponent reserves the exact tag <Self> for the current component (RFC #833), which makes
recursive components easy to write:
<template>
<li>
{{ node.id }}
<ul v-if="node.children.length">
<Self v-for="child in node.children" :key="child.id" :node="child" />
</ul>
</li>
</template>
| Context | What <Self> resolves to |
|---|---|
| SFC build with compiler metadata | the current component name from metadata |
| SFC build without metadata | a name derived from the file name |
| Direct template API | componentName when provided |
| No name available | normal component resolution |
With the flag on, <Self> wins over a local/imported component named Self (which is shadowed).
Avoid naming a component import Self; rename it instead:
<script setup lang="ts">
import OtherSelf from "./OtherSelf.vue";
</script>
<template>
<Self />
<OtherSelf />
</template>
<Self> is exact and case-sensitive; <self> is not special. Props, attrs, events, directives,
refs, v-if, v-for, v-show, and slots work as on any component, and recursion still needs an end
condition. Render functions and JSX are not covered.
Low-level template calls without an SFC file name must pass the name:
import { compile } from "@vizejs/native";
compile('<Self :node="node" />', {
componentName: "TreeNode",
experimentalSelfComponent: true,
});
Strict Slot Children
strictSlotChildren adds type checks for the children a parent passes into a slot (RFC #734). It only
affects vize check and the language server; compiled output does not change.
A component declares what each slot accepts:
import TabItem from "./TabItem.vue";
declare const Tabs: {
readonly __vizeSlots?: {
default: () => [typeof TabItem, HTMLButtonElement];
footer: () => [HTMLButtonElement];
};
};
The same contract can come from defineSlots in an SFC:
defineSlots<{
default(): [typeof TabItem, HTMLButtonElement];
footer(): [HTMLButtonElement];
}>();
Children are then checked against it:
<template>
<Tabs>
<TabItem />
<button>Open</button>
<template #footer>
<button>Save</button>
</template>
</Tabs>
</template>
Vize represents the default slot above as
$ProvidedSlotChildren<[typeof TabItem, HTMLButtonElement]> and lets TypeScript compare it with
the slot's return type. A wrong child becomes a TypeScript error:
<template>
<Tabs>
<div />
</Tabs>
</template>
| Child | Checked as |
|---|---|
Native element such as <button> |
HTMLButtonElement |
| SVG or MathML element | SVGElement or MathMLElement when no narrower type is known |
Imported component such as <TabItem> |
typeof TabItem |
| Text or interpolation | string |
Named <template #footer> |
against the named slot |
| Comments and wrappers that add no child | skipped |
| Slot return type | Meaning |
|---|---|
() => HTMLInputElement |
one input child |
() => HTMLInputElement[] |
any number of input children |
() => [HTMLInputElement, HTMLInputElement] |
exactly two input children, in order |
() => (typeof TabItem)[] |
any number of TabItem children |
() => [typeof TabItem, HTMLButtonElement] |
one TabItem, then one button |
Limits:
$ProvidedSlotChildren<__T>accepts a single child as eitherOnlyor[Only]when the slot accepts one child type.- Open slot index signatures and
anydegrade toany, so there is no strict check there. - Built-ins, dynamic components,
KeepAlive,Teleport,Transition,TransitionGroup, andSuspensedo not contribute strict child types yet. - Advanced component libraries can expose
__vizeResolveSlotswhen the contract depends on props. Components usingdefineSlotsexport a__vizeSlotsmarker for parents. - Required slot-name checks are separate; this flag only adds child type checks.