Vue RFC Experimental Details
This page expands Experimentals with opt-in contracts, examples, and tooling boundaries. Upstream RFCs remain the design sources; no RFC feature is enabled unless the matching Vize flag is explicitly on.
Opt-in Contract
Every RFC flag is independent. Enabling one proposal never enables another proposal.
| RFC | Vize config flag | Direct compiler field | Applies to | When the flag is off |
|---|---|---|---|---|
| #823 patterned templates | experimentals.patternedTemplate |
experimentalPatternedTemplate |
DOM, SSR, Vapor, SFC, WASM compile APIs | v-match, v-when, and v-case report that the opt-in is required |
| #831 in-tag comments | experimentals.inTagComment |
experimentalInTagComments |
Parser, DOM, SSR, Vapor, SFC, WASM compile APIs | // inside an opening tag is parsed as invalid tag syntax |
| #833 self references | experimentals.selfComponent |
experimentalSelfComponent |
DOM, SSR, Vapor, SFC, WASM compile APIs | <Self> is an ordinary component tag |
| #734 strict slot children | experimentals.strictSlotChildren |
experimentalStrictSlotChildren |
vize check, LSP, and virtual-TS project APIs |
no extra child-type assertions are generated |
Use experimentals in shared config and plugin config. Use the direct compiler fields only in an
integration that has already resolved aliases, precedence, and {} switch objects to final booleans.
import { defineConfig } from "vize";
export default defineConfig({
experimentals: {
inTagComment: true,
patternedTemplate: true,
selfComponent: true,
strictSlotChildren: true,
},
});
compile(templateSource, { experimentalPatternedTemplate: true });
compileVapor(templateSource, { experimentalSelfComponent: true, componentName: "TreeNode" });
parseTemplate(templateSource, { experimentalInTagComments: true });
compileSfc(sfcSource, { filename: "TreeNode.vue", experimentalStrictSlotChildren: true });
Direct native fields on compile, compileVapor, parseTemplate, and compileSfc* do not
understand aliases such as pattenedTemplate or intagComment, and they do not accept {} switch
objects. Direct Vite plugin values still win over shared config, including explicit opt-outs.
Current Scope
| RFC | Vize ships today | Boundary to keep explicit |
|---|---|---|
| #823 | parser support, runtime lowering, branch-local bindings, guards, rest/as patterns, diagnostics for flag-off and invalid placement | Canon checks narrowing/coverage; complete editor navigation and compiler grammar convergence remain deferred |
| #831 | parser support for in-tag //, source text in root.comments, and compile pipelines that preserve the AST comment |
no runtime output, no child comment node, and no browser in-DOM template support |
| #833 | exact <Self> current-component resolution in DOM, SSR, and Vapor compilation |
no render-function or JSX macro; a local/imported component named Self is shadowed when the flag is enabled |
| #734 | virtual TypeScript assertions for provided default and named slot children | no template codegen change; open slot contracts and any degrade to TypeScript's own permissive checks |
For entry-point and proof checklists, see Experimentals Reference.
Patterned Templates
patternedTemplate implements the long-form v-match / v-when syntax from RFC #823. The subject expression is evaluated once, direct branch children are tested in source order, and only the first matching branch renders. Pattern property reads are captured once per attempted arm. Guards and rendered bindings receive the same captured values, including rest-copy identity. Rest copies are deferred until the entire shape matches; failed shapes and later arms do not evaluate their guards. v-when cannot share its host with v-if, v-else-if, v-else, v-for, or v-match.
Inline HTML SFCs accept outer <template v-match> with nested-match semantics in DOM, SSR, and Vapor. Header-only subject edits invalidate HMR; parsed template.content remains the original block body.
External src, preprocessors, and descriptors missing original source metadata are rejected. Canon supports opt-in narrowing and coverage checks below; assembled compiler SFC source maps remain script-only.
Croquis accepts analyzeSfc(source, { experimentalPatternedTemplate: true }) and the Playground Croquis checkbox for root and nested matches. Its strict RFC parser records branch-local declarations, enclosing value lookups, guard references, and syntax diagnostics with authored ranges (including HTML entities). Canon uses the same parser through typeCheck(source, { experimentalPatternedTemplate: true, includeVirtualTs: true }), the Playground Canon checkbox, and vize check with experimentals.patternedTemplate. Native Maestro reads the same workspace opt-in: it reports structural and missing-coverage errors, preserves unreachable-arm warnings, and rechecks unsaved edits. Root-pattern binding hover and definition return the narrowed type and authored declaration. Restart the language server after changing the workspace flag. DOM, Vapor, and SSR now share this pattern parser and use pattern as name; the obsolete as const name spelling is rejected. Content Mapper opt-in routing and complete LSP rename/completion coverage for guards, nested closures, and or-pattern bindings remain tracked in #6176.
<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>
Bindings are branch-local. They are visible to the element carrying v-when, its attributes and
directives, its guard expression, and its children. Sibling branches cannot read them.
Supported branch patterns:
| Pattern | Example | Runtime check |
|---|---|---|
| Literal | v-when="'ready'", v-when="404" |
strict equality |
| Value | v-when="Status.Ready" |
SameValueZero comparison with an identifier or member expression, including NaN |
| Wildcard | v-when="_" |
always matches and introduces no binding |
| Const binding | v-when="const value" |
always matches and binds value in this branch |
| Object | v-when="{ kind: 'ok', value: const data }" |
open structural object match; extra properties are allowed |
| Object shorthand | v-when="{ kind: 'ok', const data }" |
{ const data } means { data: const data } |
| Object rest | v-when="{ kind: 'error', ...const payload }" |
binds remaining own enumerable properties; lone ... is accepted |
| Array / tuple | v-when="[const first, ...const rest]" |
requires Array.isArray; rest allows additional items |
| Or | `v-when="'idle' | 'loading'"` |
| As binding | v-when="{ kind: 'ok' } as whole" |
binds the matched value as whole |
| Guard | v-when="{ error: const e } if (e.retriable)" |
runs after the pattern succeeds |
Patterned Diagnostics
Without the flag, Vize reports the required opt-in instead of passing unknown directives through:
<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 top-level 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>
let and var bindings are rejected. const is the only binding declaration Vize accepts today.
v-when also rejects directive arguments and modifiers. v-case and v-case.default are kept only
as compatibility aliases for older Vize experiments; new templates should use v-when and _.
Patterned Type Boundary
Canon narrows arm bindings and the original subject, checks missing coverage, and reports unreachable arms as warnings. The ordinary checked pattern_matching.d.ts declarations are shared once per TypeScript program, not copied into each template.
Guards never prove coverage; optional property presence retains undefined and the missing-key space. Finite object/tuple unions, readonly arrays and rest bindings are supported. Open primitives, any, unknown, and union-valued value patterns remain conservative; use an unguarded v-when="_" when coverage cannot be proven.
Missing coverage is an error anchored to the authored subject and fails vize check; warning-only unreachable arms do not fail it.
WASM alone supplies structural diagnostics and virtual TypeScript; full type diagnostics require the Playground's Monaco TypeScript worker (or the native checker). Other virtual-TS hosts must register virtualTsHelpers once as an ambient declaration file.
The full upstream type-tooling acceptance contract for narrowing and exhaustiveness also includes editor navigation and completion. Editor diagnostics still use the disabled opt-in; editor/Content Mapper option routing, navigation and completion are not enabled by this Canon support.
Binding inside an or-pattern alternative is also deferred: split cases needing bindings into separate branches. The RFC shorthand candidates ?=, |=, and ~= are not public Vize branch syntax.
In-Tag Comments
inTagComment implements compile-time-only // comments inside an opening tag's attribute list from
RFC #831. Vize keeps their source text for parser/tooling fidelity and does not emit runtime code.
<template>
<LegacySelect
:options="options"
// @vue-expect-error legacy API accepts string IDs
:selected-id="selectedId"
/>
</template>
Allowed placements are after the tag name, between complete attributes/directives, and after the
last attribute before > or />. Put the closing delimiter on the next line after a trailing
comment: // consumes the closing delimiter when > or /> stays on the same line. The syntax is
not valid inside a tag name, attribute name, directive name, argument, modifier, or attribute value.
The comment is stored on the template root comments list with CommentKind::InTag. It is not an
element prop, not a child <!-- ... --> node, and not controlled by the normal template comments
option; comments: false still preserves in-tag comments for tooling. // inside an attribute value
remains text:
<template>
<a href="https://example.test/a//b">Link</a>
</template>
This syntax is intended for SFC and tooling pipelines that parse templates themselves. 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 from RFC #833.
<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>
Resolution rules:
| Context | How Vize resolves <Self> |
|---|---|
| SFC build with compiler metadata | uses the current component name from metadata |
| SFC build without metadata | derives the component name from the file name |
| Direct template API | uses componentName when provided |
| No name available | keeps normal component resolution semantics |
When the flag is enabled, <Self> is resolved before local, imported, or global components named
Self. Avoid naming a component import Self; rename the import if you need the other component.
Props, attrs, events, directives, refs, v-if, v-for, v-show, and slots behave like any other
component usage. Recursive templates still need a termination condition.
<script setup lang="ts">
import OtherSelf from "./OtherSelf.vue";
</script>
<template>
<Self />
<OtherSelf />
</template>
The tag is exact and case-sensitive. <Self> is reserved only when the flag is enabled; <self> is
not special. Render functions and JSX are outside this flag.
Low-level template calls must provide a name explicitly when no SFC filename is available:
import { compile } from "@vizejs/native";
compile("<Self :node=\"node\" />", {
componentName: "TreeNode",
experimentalSelfComponent: true,
});
Strict Slot Children
strictSlotChildren implements the tooling side of RFC #734. It adds virtual TypeScript assertions
for the child nodes a parent provides to a component slot. It does not change template codegen or
runtime rendering.
import TabItem from "./TabItem.vue";
declare const Tabs: {
readonly __vizeSlots?: {
default: () => [typeof TabItem, HTMLButtonElement];
footer: () => [HTMLButtonElement];
};
};
The same public contract can come from defineSlots in an SFC:
defineSlots<{
default(): [typeof TabItem, HTMLButtonElement];
footer(): [HTMLButtonElement];
}>();
<template>
<Tabs>
<TabItem />
<button>Open</button>
<template #footer>
<button>Save</button>
</template>
</Tabs>
</template>
With the flag enabled, Vize represents the provided default slot as
__VizeProvidedSlotChildren<[typeof TabItem, HTMLButtonElement]> and asks TypeScript to compare it
with the component's __vizeSlots.default return type. A mismatched child becomes a TypeScript
diagnostic:
<template>
<Tabs>
<div />
</Tabs>
</template>
Child mapping:
| Provided child | Virtual TypeScript type |
|---|---|
Native element such as <button> |
HTMLButtonElement |
| SVG or MathML element | SVGElement or MathMLElement fallback when no narrower DOM type is known |
Imported component such as <TabItem> |
typeof TabItem |
| Text or interpolation | string |
Named <template #footer> |
checked against the named slot |
| Comments and structural-only wrappers | skipped when they do not contribute a child value |
RFC #734's return shapes are preserved as TypeScript cardinality contracts:
| Slot return contract | Meaning in Vize virtual TS |
|---|---|
() => HTMLInputElement |
one input child, accepting the single-child shorthand |
() => HTMLInputElement[] |
zero or more input children |
() => [HTMLInputElement, HTMLInputElement] |
exactly two input children in tuple order |
() => (typeof TabItem)[] |
zero or more TabItem component children |
() => [typeof TabItem, HTMLButtonElement] |
one TabItem child followed by one button child |
__VizeProvidedSlotChildren<__T> accepts a single child as either Only or [Only] when the slot
contract has one accepted child type, and preserves tuple/array shapes for multi-child contracts.
Open slot index signatures and any degrade to any, so TypeScript will not produce strict child
diagnostics there. Built-ins, dynamic components, KeepAlive, Teleport, Transition,
TransitionGroup, and Suspense do not currently contribute strict component child types.
Advanced component libraries can expose __vizeResolveSlots when the slot contract depends on props.
Components that use defineSlots export a __vizeSlots marker for parents. Required slot-name
checks are separate; this flag adds child node type checks on top of the existing virtual-TS slot
model.
Verification Checklist
Before documenting a new experimental behavior, pin all of these facts in tests:
- the flag defaults to off
- shared config and direct plugin options resolve with the documented precedence
- direct compiler fields are final booleans
- low-level docs name the real entry points:
compile,compileVapor,parseTemplate, andcompileSfc* - bad syntax produces diagnostics instead of silently compiling
- DOM, SSR, Vapor, SFC, WASM, or
vize checkcoverage matches the surface matrix - docs include a good example, a flag-off or invalid-placement example, and an implementation boundary