Vaadin components come with TypeScript definitions helping to use web components in TypeScript views.
The type definitions are d.ts
files generated every time when new release is published to npm.
Support for TypeScript definitions is added in Vaadin 17. In most of cases, it does not require any changes to the code. At the same time, using proper types for the web components helps to make the client side views more reliable. Depending on the IDE you use, TypeScript definitions can also give additional benefits like better code completion and auto import.
If you are using Visual Studio Code, we recommend to install lit-plugin which provides syntax highlighting and type checking support for LitElement templates. You can also use lit-analyzer CLI tool by the same author for type checking.
Every TypeScript definition file exports type declarations, which describe the public API provided by the web component. In order to use the type declarations in your client side views, you need to import them using type-only import syntax:
import type { DialogElement } from '@vaadin/vaadin-dialog';
The type declarations are only used by the TypeScript compiler to statically analyze the code and
collect information about types. They are removed during webpack
compilation step and will not end
up in the resulting JavaScript bundle.
In some cases you might need to get a reference to the web component, and use it to set properties or call methods. Below you will find examples of how to improve developer experience in such cases by applying proper types.
The suggested way of storing a reference in LitElement based view is @query
decorator. When using
it, you need to provide a correct type for the component you are referring to:
@query('#dialog')
private dialog!: DialogElement;
Note the !
sign used in the property declaration. It is a definite assignment assertion
operator. This way we are telling the TypeScript compiler that dialog
property is initialized indirectly,
and therefore it does not need a default value.
Some Vaadin components like Grid and Dialog allow to pass the <template>
element and then use it
to stamp the content. This API is not compatible with lit-html, so in LitElement-based views we
recommend using renderer
functions instead.
Renderer function is a class method that accepts one or several arguments. In order to access them without TypeScript warnings, you need to get them properly typed by importing and using corresponding type declarations:
protected indexRenderer(root: HTMLElement, column: GridColumnElement, model: GridItemModel) {
render(html`<div>${model.index}</div>`, root);
}
Here we use GridItemModel
type declaration exported by the @vaadin/vaadin-grid
npm package. It is a
TypeScript interface describing the properties available on the model
, including index
. We also use GridColumnElement
declaration.
See also the full example of using renderer
functions on the Grid columns, including some aspects related to making this
work in renderers. There are similar examples for Select, Dialog, ContextMenu and Notification components.
When using event listeners in LitElement-based views, we might want to get the reference to the
event target and then access its value. One common case for this is handling a change
event.
While we typically only use one listener per element, the lit-html syntax for binding listeners does not let TypeScript know what type of component it is. So we should care about it ourselves:
render() {
return html`
<vaadin-text-field @change="${this.onChange}"></vaadin-text-field>
`
}
onChange(event: Event) {
const field = event.composedPath()[0] as TextFieldElement;
// <vaadin-text-field> has a value property.
console.log(field.value);
}
We use as
syntax, which is a type assertion, often called "type cast". It is a hint for the TypeScript compiler forcing it to use the type that we provide. In this case we are confident about the type, but generally type casts should be avoided.
When creating your own custom elements for using with client side views, you might want to instruct TypeScript to use your definitions. This is not strictly required, but sometimes it improves developer experience and allows to write less code.
As an example, let's look into using querySelector
and querySelectorAll
methods with your own
custom elements. These methods return Element
, so the easiest workaround would be probably to use
a type cast:
const items = this.renderRoot.querySelectorAll('color-item') as ColorItem[];
items.forEach(item => {
// access item properties
});
However, this approach isn't clean, as it requires to write as ColorItem[]
every time the method
is called. There is a better alternative: registering a class corresponding to the HTML tag name in
the built-in HTMLElementTagNameMap
interface:
declare global {
interface HTMLElementTagNameMap {
'color-item': ColorItem;
}
}
Now, every time when you call querySelector
or querySelectorAll
with a corresponding tag name,
TypeScript compiler will infer the proper type automatically, making the type cast no longer necessary:
const items = this.renderRoot.querySelectorAll('color-item');
items.forEach(item => {
// access item properties
});
The TypeScript definitions for Vaadin components provide these registrations, so you don't have to
use type casts. Apart from the query methods, this applies to few other methods, such as createElement
and closest
.
The current implementation of Vaadin components has limitations related to using TypeScript
definitions. They are partially caused by the fact that the components are written in JavaScript,
and the d.ts
files are generated
from JSDoc comments.
Vaadin components dispatch custom events, such as value-changed
. These events are different from
the native DOM events like click
. Firstly, they have detail
property, which has type of any
.
This means that TypeScript does not have any information about the properties that might exist on
the detail
object for the particular custom event.
Another problem affecting developer experience
is the fact that custom events can not be used in addEventListener
directly. Attempting to do it
would result in the TypeScript compilation error "No overload matches this call".
The possible workaround for it would be using another built-in interface called HTMLElementEventMap
. You
can add the following code to prevent TypeScript from complaining about incorrect addEventListener
usage:
declare global {
interface HTMLElementEventMap {
'value-changed': CustomEvent;
}
}
The challenge is that different Vaadin components might use different types for the same value
property. So this is not something we currently support. We consider this an enhancement and not a
bug. See also the tracking issue.
Certain Vaadin components, namely Grid, ComboBox and CRUD, support setting items
property as an
array of objects. Typically, when using a component, we know what type of objects we expect, and
we prefer to only declare it once.
In TypeScript, this could be achieved using generic types. However,
because of the way the components are implemented, we would preferably need to infer the items
type
also in the renderer functions, as the model.item
argument type.
This feature appears to be non-trivial, keeping in mind that we generate type definitions from JSDoc.
So we decided to use unknown[]
for the items
property type, and then use type cast in the renderers:
nameRenderer(root: HTMLElement, column: GridColumnElement, model: GridItemModel) {
const user = model.item as User;
render(html`<div>${user.firstName} ${user.lastName}</div>`, root);
}
While using type casts is not the best idea in terms of type safety and developer experience, we do not have a better option at the moment. So for now we recommend this approach. Please see the issue where this enhancement is being tracked.
We are working on improving our documentation to provide more components examples and recipes in TypeScript. While this work is in progress, check out TypeScript Vaadin examples project for live demos of using Vaadin components.
If you would like to request a code example that is missing from the live demos, feel free to submit an issue and describe your problem. We aim to make the developer experience with TypeScript definitions as smooth as possible.
exporting types from package roots is important for discoverablility - e.g. currently
vaadin-charts
exposes its types from a file in/dist
, it should export from the root instead