TypeScript Definitions of Components

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.

In most of cases, TypeScript support does not require any changes to the code. At the same time, using proper types for the web components helps making the client side views more reliable. Depending on the IDE you use, TypeScript definitions can also enable 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.

Importing Type Declarations

Every TypeScript definition file exports type declarations, which describe the public API provided by the web component. 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 do not end up in the resulting JavaScript bundle.

Using Type Declarations

In some cases you might need to get a reference to the web component, and use it to set properties or call methods. The following examples show how to improve the developer experience in such cases by applying proper types.

Query Decorator

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 tells the TypeScript compiler that the dialog property is initialized indirectly and does not need a default value.

Renderer Functions

Renderer function is a class method that accepts one or several arguments. To access them without TypeScript warnings, you need to get them typed by importing and using the 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, Context Menu and Notification components.

Event Listeners

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 a "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.

Custom Events

Vaadin components offer the set of type definitions for custom events making it easier to use them in TypeScript. In particular, this allows to get proper type for the detail property, which has type of any by default. To use the event types, you need to import them manually:

import type { TextFieldInvalidChanged } from '@vaadin/vaadin-text-field';

render() {
  return html`
    <vaadin-text-field @invalid-changed="${this.onInvalidChanged}"></vaadin-text-field>
  `
}

onInvalidChanged(event: TextFieldInvalidChanged) {
  // `detail` is an object with boolean `value` property.
  console.log(event.detail.value);
}

Type definitions for custom events are available for these components:

  • Checkbox

  • Checkbox Group

  • Combo Box

  • Context Menu

  • CRUD

  • Custom Field

  • Date Picker

  • Date Time Picker

  • Grid

  • Grid Pro

  • List Box

  • Login

  • Menu Bar

  • Rich Text Editor

  • Select

  • Tabs

  • Text Field

  • Time Picker

  • Upload

Registering Elements

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 mandatory, but in some cases 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. This allows to avoid writing type casts when using certain DOM methods. Apart from the query methods, this applies to other methods, such as createElement and closest.

Limitations

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.

Items Property

Certain Vaadin components, namely Grid, Combo Box 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. In future we will provide a cleaner solution for declarative rendering of such components. Please see the issue where this enhancement is being tracked.

Examples

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.