Hilla Documentation

TypeScript Definitions of Components

Understanding how to use Vaadin components' TypeScript definitions with web components in TypeScript views.

Vaadin components come with TypeScript definitions, facilitating the use of web components in TypeScript views. The type definitions are d.ts files generated every time a new release is published to npm.

In most cases, TypeScript support 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 enable better code completion and auto-import.

When using Visual Studio Code, it is recommended to install the lit-plugin, which provides syntax highlighting and type-checking support for Lit templates. You can also use the 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 the type-only import syntax:

import type { Dialog } from '@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 the webpack compilation step and do not end up in the resulting JavaScript bundle.

The following sections give examples of how to improve the developer experience when using Vaadin components by applying correct types for elements, their properties, and events.

Query Decorator

Although Lit discourages manual DOM operations in favor of binding data to the component template declaratively using reactive properties, it can sometimes be convenient to get a reference to the component instance and use it for method calls.

The suggested way of storing a reference in Lit-based views is the @query decorator. This expects the type of the corresponding web component to be specified:

@query('#dialog')
private dialog!: Dialog;

Note the ! sign used in the property declaration. This is the definite assignment assertion operator. It tells the TypeScript compiler that the dialog property is initialized indirectly and does not need a default value.

Event Listeners

When using event listeners in Lit-based views, it might be useful to get a reference to the event target and then access its value. One common case for this is handling a change event.

The Lit syntax for binding listeners does not let TypeScript know what type of component the listener belongs to, so this needs to be handled manually by specifying the correct type:

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

onChange(event: Event) {
  const field = event.composedPath()[0] as TextField;
  // <vaadin-text-field> has a value property.
  console.log(field.value);
}

The as keyword is a type assertion, often also called a "type cast". It is a hint for the TypeScript compiler, forcing it to use the type provided explicitly. In this particular case, it is safe, as the component that dispatches the event is known in advance.

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 you to get a 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 { TextFieldInvalidChangedEvent } from '@vaadin/text-field';

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

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

Renderer Functions

Some Vaadin components, such as Grid and Dialog, allow you to pass the <template> element and then use it to stamp the content. This API is not compatible with Lit templates. Use renderer functions in Lit-based views instead.

A renderer function is a class method that accepts one or more 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: GridColumn,
  model: GridItemModel<Person>
) {
  render(html`<div>${model.index}</div>`, root);
}

The GridItemModel type declaration is exported by the @vaadin/grid npm package. It is a TypeScript interface describing the properties available on the model, including index. The GridColumn declaration is the type of the <vaadin-grid-column> component.

See also the Grid content example, which demonstrates how to define renderer functions on the Grid columns to provide rich content based on the column.

Generic Types

Certain Vaadin components, namely Grid, Combo Box, CRUD, and Virtual List, support setting the items property as an array of objects. Typically, when you use a component, you know the expected type of the object in advance and can provide it explicitly.

In TypeScript, this could be achieved using generic types. For example, a Grid type can be specified using @query decorator:

@query('#grid')
private grid!: Grid<Person>;

Note that this type does not get inferred by the component internally. Hence, the same generic type needs to be passed to the model argument of the renderer function:

nameRenderer(
  root: HTMLElement,
  column: GridColumn,
  model: GridItemModel<Person>
) {
  // `model` is an object with an `item` property of type `Person`
  const person = model.item;
  render(html`<div>${person.firstName} ${person.lastName}</div>`, root);
}

A type argument can be also used in event listeners to detect changes of some properties:

onSelectedItemChanged(event: ComboBoxSelectedItemChangedEvent<Person>) {
  // `detail` is an object of a `value` property of type `Person`
  console.log(event.detail.value);
}

Generic type arguments can be passed to various properties and TypeScript interfaces listed below.

Combo Box Generic Types

The following Combo Box properties support generic types:

  • dataProvider

  • filteredItems

  • items

  • renderer

  • selectedItem

The following Combo Box interfaces support generic type arguments:

  • ComboBoxDataProvider

  • ComboBoxDataProviderCallback

  • ComboBoxItemModel

  • ComboBoxRenderer

  • ComboBoxSelectedItemChangedEvent

CRUD Generic Types

The following CRUD properties support generic types:

  • dataProvider

  • editedItem

  • items

The following CRUD interfaces support generic type arguments:

  • CrudCancelEvent

  • CrudDataProviderCallback

  • CrudDataProvider

  • CrudDeleteEvent

  • CrudEditEvent

  • CrudEditedItemChangedEvent

  • CrudItemsChangedEvent

  • CrudSaveEvent

Grid Generic Types

The following Grid properties support generic types:

  • activeItem

  • cellClassNameGenerator

  • dataProvider

  • dragFilter

  • dropFilter

  • expandedItems

  • items

  • rowDetailsRenderer

  • selectedItems

The following Grid column properties support generic types:

  • footerRenderer

  • headerRenderer

  • renderer

The following Grid interfaces support generic type arguments:

  • GridActiveItemChangedEvent

  • GridBodyRenderer

  • GridCellActivateEvent

  • GridCellClassNameGenerator

  • GridCellFocusEvent

  • GridColumnReorderEvent

  • GridColumnResizeEvent

  • GridDataProvider

  • GridDragAndDropFilter

  • GridDragStartEvent

  • GridDropEvent

  • GridExpandedItemsChangedEvent

  • GridEventContext

  • GridItemModel

  • GridRowDetailsRenderer

  • GridSelectedItemsChangedEvent

Virtual List Generic Types

The following Virtual List properties support generic types:

  • items

  • renderer

The following Virtual List interfaces support generic type arguments:

  • VirtualListItemModel

  • VirtualListRenderer

Registering Elements

When creating your own custom elements to use with client-side views, you might want to instruct TypeScript to use your definitions. This is not mandatory, but sometimes it improves the developer experience and allows you to write less code.

As an example, let us look at using the querySelector() and querySelectorAll() methods with your own custom elements. These methods return Element, so the easiest workaround would probably be to use a type cast:

const items = this.renderRoot.querySelectorAll('color-item') as ColorItem[];
items.forEach(item => {
  // access item properties
});

However, this approach is not clean, as it requires us to write as ColorItem[] every time the method is called. A better alternative would be to register a class corresponding to the HTML tag name in the built-in HTMLElementTagNameMap interface:

declare global {
  interface HTMLElementTagNameMap {
    'color-item': ColorItem;
  }
}

Now, every time you call querySelector() or querySelectorAll() with a corresponding tag name, the TypeScript compiler can 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 you 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().