Phase B of the Vue types proof of concept for
vaadin/web-components,
branch proto/vue-types. The question: once every package ships a
generated vue.d.ts, do thin Vue wrappers still add enough value?
The page shows the same components in two ways:
src/components/– thin wrappersVTextField,VComboBox,VButtonbuilt on the generated*Propstypes.src/App.vueandsrc/Overlays.vue– the bare custom elements, typed only through thevue.d.tsfiles intypes/.
npm install
npm run sync-types # needs ../../vaadin/wc on branch proto/vue-types
npm run typecheck # vue-tsc with strictTemplates, plus a lib check of types/
npm run devsync-types runs the generator from the web-components checkout against the
installed @vaadin/* packages and copies the result into types/. Once the
files ship with the packages, tsconfig.json can include
node_modules/@vaadin/*/vue.d.ts directly.
The sample uses @vaadin/*@next (25.3.0-beta1). The 25.2 manifests do not
list the theme attribute, so theme="primary" fails to type-check there.
<script setup lang="ts">
import "@vaadin/text-field";
import type { TextFieldValueChangedEvent } from "@vaadin/text-field";
import type { TextFieldProps } from "../../types/vaadin-text-field";
type Props = Omit<TextFieldProps, "value" | "onValueChanged">;
const props = defineProps<Props>();
const model = defineModel<string>({ default: "" });
</script>
<template>
<vaadin-text-field
v-bind="forwarded"
:value="model"
@value-changed="onValueChanged"
/>
</template>defineProps<Omit<TextFieldProps, …>>()works with the imported type. Vue reads the literalbooleantypes invue.d.tsas runtimeBooleanprops, which is what makes<VTextField required>work.forwardeddropsundefinedprops, so the wrapper does not reset element state on re-render.VComboBoxturns therendererproperty into anitemscoped slot.VButtonmaps named slots to light DOMslotattributes and re-emitsclick.
Checked with Playwright against vite preview:
| Case | Wrapper | Bare element with types |
|---|---|---|
| Typing updates the model | v-model |
:value + @value-changed |
| Selecting a combo-box item | v-model |
same handler pattern |
| Custom item rendering | #item slot, re-renders |
renderer function, no templates |
required boolean attribute |
required === true |
required === "", so not required |
theme, label, error-message, @click |
pass through | work directly |
| Wrong prop or handler type | error | error |
The required row matters. Vue sets a DOM property when key in el, and it
turns an empty attribute value into true only when the property already
holds a boolean. Vaadin boolean properties default to undefined, so the
bare element gets required = "". Users must write :required="true", or
the wrapper declares a Boolean prop. Defaulting boolean properties to
false in the components would fix this for every Vue user.
src/Overlays.vue uses the bare elements with the slotted APIs from 25.3:
<vaadin-select-list-box slot="overlay">,
<vaadin-context-menu-list-box slot="overlay">, and dialog content with
header-title and the footer slot. Checked the same way:
| Case | Result |
|---|---|
Select options from v-for |
selection updates the model, label shows |
| Option added later | appears and is selectable |
Menu item with @click |
handler runs, menu closes |
Menu item under v-if |
toggles between openings |
Dialog body with v-model, footer close |
closes, closed fires, draft state stays |
| Esc on the dialog | @opened-changed sets opened back to false |
The sub-elements (vaadin-select-item, vaadin-context-menu-item, both
list-boxes) are in GlobalComponents, so the nested markup type-checks.
None of the three needs a wrapper. v-model on the select is the only gap,
and it is the same defineModel recipe as VTextField.
Two notes:
- Vue's
HTMLAttributeshas noslot, soslot="footer"fails understrictTemplates. The generated types now add it. idon a wrapper is rejected understrictTemplates, because wrappers declare only the element's props. It still works at runtime.
- Types alone cover bindings, events, hover docs, and template errors.
- Wrappers add
v-model, Vue slots for renderers, and correct boolean attributes. - Both type-check under
strictTemplatesand share the generated*Props.