How to Use Web Components with TypeScript

Learn how to effectively use TypeScript with web components to build scalable, maintainable, and type-safe applications for modern web projects.

Web components have emerged as a powerful tool in modern web development, allowing developers to create reusable, modular elements that can be used across different projects and frameworks. These components provide a way to encapsulate functionality, ensuring that each piece of your application is self-contained and easy to manage. TypeScript, on the other hand, has become the go-to language for developers who want to add static typing to JavaScript, making their code more reliable and easier to maintain.

Combining web components with TypeScript offers a robust approach to building scalable web applications. By leveraging the strengths of both technologies, developers can create components that are not only reusable and modular but also type-safe and easier to debug. In this article, we will explore how to use web components with TypeScript, covering everything from setting up your development environment to creating and using your own custom elements.

Setting Up Your Development Environment

Before you start building web components with TypeScript, it's essential to set up a development environment that supports both technologies. This will ensure a smooth workflow and allow you to take full advantage of the features provided by TypeScript and modern web development tools.

Before you start building web components with TypeScript, it’s essential to set up a development environment that supports both technologies. This will ensure a smooth workflow and allow you to take full advantage of the features provided by TypeScript and modern web development tools.

Installing Node.js and npm

The first step in setting up your environment is to install Node.js and npm (Node Package Manager). Node.js provides the runtime for executing JavaScript code outside of a browser, while npm is used to manage packages and dependencies. If you haven’t installed Node.js yet, you can download it from the official website.

Once installed, verify that Node.js and npm are correctly set up by running the following commands in your terminal or command prompt:

node -v
npm -v

These commands will display the installed versions of Node.js and npm, confirming that your environment is ready to go.

Creating a New TypeScript Project

With Node.js and npm installed, the next step is to create a new TypeScript project. Start by creating a new directory for your project and navigating into it:

mkdir typescript-web-components
cd typescript-web-components

Next, initialize a new Node.js project by running:

npm init -y

This command will generate a package.json file with the default settings, which you can modify later if needed.

Now, you need to install TypeScript as a development dependency:

npm install typescript --save-dev

After installing TypeScript, create a tsconfig.json file to configure TypeScript for your project:

npx tsc --init

This will generate a basic tsconfig.json file. Open this file in your code editor and ensure that the following settings are enabled or added:

{
  "compilerOptions": {
    "target": "es6",
    "module": "es6",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

These settings ensure that TypeScript compiles your code to ECMAScript 6 (ES6), handles module resolution correctly, and outputs the compiled files to a dist directory while keeping the source files in a src directory.

Installing Web Component Dependencies

With TypeScript set up, the next step is to install the necessary dependencies for creating web components. For this tutorial, we'll use lit, a lightweight library that simplifies the creation of web components with expressive templates.

With TypeScript set up, the next step is to install the necessary dependencies for creating web components. For this tutorial, we’ll use lit, a lightweight library that simplifies the creation of web components with expressive templates.

Install lit using npm:

npm install lit

Now that you have lit installed, you’re ready to start building your first web component with TypeScript.

Setting Up the Project Structure

To keep your project organized, create the following directory structure:

typescript-web-components/
│
├── src/
│   └── my-component.ts
├── dist/
├── index.html
└── tsconfig.json
  • src/: This directory contains your TypeScript source files. The my-component.ts file will be where you define your custom web component.
  • dist/: This directory will hold the compiled JavaScript files after running the TypeScript compiler.
  • index.html: This is the main HTML file that you will use to test your web component.

Compiling TypeScript to JavaScript

Before we start writing code, it’s important to understand how TypeScript compiles into JavaScript. Whenever you write TypeScript code, it needs to be compiled into JavaScript because browsers cannot execute TypeScript directly.

To compile your TypeScript files, you can use the following command:

npx tsc

This command will compile all TypeScript files in the src directory and output the JavaScript files in the dist directory, as configured in your tsconfig.json file.

To automate this process and compile your TypeScript files whenever they change, you can add a watch script to your package.json file:

{
  "scripts": {
    "watch": "tsc --watch"
  }
}

Now, you can run the following command to start the TypeScript compiler in watch mode:

npm run watch

This will keep the TypeScript compiler running in the background, automatically recompiling your code whenever you save changes.

With your development environment set up, you’re now ready to start building web components with TypeScript. In the next section, we’ll create a simple custom element using lit and explore how TypeScript enhances the development process.

Creating Your First Web Component with TypeScript

Now that your development environment is ready, it’s time to create your first web component using TypeScript. We’ll start with a simple example to get you familiar with the basic concepts of web components and how TypeScript can enhance the development process.

Defining a Custom Element

In this example, we’ll create a custom element called my-component that displays a greeting message. We’ll use the LitElement class from the lit library to simplify the creation and management of our web component.

First, open the my-component.ts file in the src directory and start by importing the necessary modules:

import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
  • LitElement: This is the base class provided by lit for creating web components. It includes built-in support for reactive properties, rendering templates, and more.
  • html: This is a tagged template literal function used to define the HTML template for your component.
  • css: This is a tagged template literal function used to define scoped CSS for your component.
  • customElement and property: These are decorators provided by lit to simplify the registration of custom elements and the declaration of reactive properties.

Next, define the MyComponent class by extending LitElement:

@customElement('my-component')
export class MyComponent extends LitElement {
  @property({ type: String }) name = 'World';

  static styles = css`
    :host {
      display: block;
      padding: 16px;
      background-color: #f0f0f0;
      border-radius: 8px;
      text-align: center;
      font-family: Arial, sans-serif;
    }
  `;

  render() {
    return html`
      <h1>Hello, ${this.name}!</h1>
    `;
  }
}

Breaking Down the Code

Here’s what each part of the MyComponent class does:

  • @customElement('my-component'): This decorator registers the MyComponent class as a custom element with the tag name my-component. Once registered, you can use <my-component></my-component> in your HTML to render this element.
  • Reactive Property with @property: The name property is declared as a reactive property using the @property decorator. This means that any time the name property changes, the component will automatically re-render. In this case, name is a string that defaults to 'World'.
  • Styles with css: The styles property uses the css tagged template literal to define scoped styles for the component. These styles are applied only to this component, ensuring that they do not leak out and affect other parts of the page.
  • Rendering HTML with html: The render method returns the HTML template for the component, using the html tagged template literal. The template includes a heading that displays a greeting message, which incorporates the name property.

Using the Web Component in HTML

Now that you've defined your custom element, it's time to use it in your index.html file. Open the index.html file in the root directory of your project and add the following code:

Now that you’ve defined your custom element, it’s time to use it in your index.html file. Open the index.html file in the root directory of your project and add the following code:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My First Web Component with TypeScript</title>
</head>
<body>
  <my-component name="TypeScript"></my-component>

  <script type="module" src="./dist/my-component.js"></script>
</body>
</html>

Explanation of the HTML File

  • Custom Element Usage: The <my-component name="TypeScript"></my-component> element uses the my-component custom element that you defined earlier. The name attribute is passed to the component, which will update the greeting message accordingly.
  • JavaScript Module: The <script> tag with type="module" is used to load the compiled JavaScript file from the dist directory. This script contains the compiled code for your my-component.ts file.

Compiling and Running the Component

With everything set up, it’s time to compile your TypeScript code into JavaScript and run your web component. If you’re running the TypeScript compiler in watch mode (npm run watch), the code should already be compiled. Otherwise, run the following command to compile your TypeScript files:

npx tsc

After the code is compiled, open the index.html file in your browser. You should see a greeting message that says “Hello, TypeScript!” rendered by your custom web component.

Exploring the Benefits of TypeScript

Using TypeScript with web components offers several advantages:

  • Type Safety: TypeScript provides static type checking, which helps catch errors at compile time rather than at runtime. This leads to more reliable code and fewer bugs.
  • Enhanced Tooling: TypeScript integrates well with modern code editors, offering features like autocompletion, type inference, and intelligent code navigation. This improves the overall development experience.
  • Better Documentation: TypeScript’s type annotations serve as documentation for your code, making it easier for other developers (and your future self) to understand how your components are intended to be used.

With your first web component successfully created and running, you’re now ready to explore more advanced features and techniques. In the next section, we’ll dive into how to manage complex state and interactivity in web components using TypeScript.

Managing State and Interactivity in Web Components with TypeScript

Building interactive and dynamic web components is where the true power of combining TypeScript with web components becomes evident. As your components grow in complexity, managing state and handling user interactions efficiently becomes crucial. TypeScript's static typing and robust features can help you create more maintainable and error-free components while ensuring that your web application remains responsive and user-friendly.

Building interactive and dynamic web components is where the true power of combining TypeScript with web components becomes evident. As your components grow in complexity, managing state and handling user interactions efficiently becomes crucial.

TypeScript’s static typing and robust features can help you create more maintainable and error-free components while ensuring that your web application remains responsive and user-friendly.

Using Reactive Properties for State Management

In web components, managing state often involves using properties that change over time in response to user interactions or external data updates. In the previous example, we used the @property decorator to define a simple reactive property.

Reactive properties are the cornerstone of state management in web components, as they trigger automatic re-renders when their values change, ensuring that the UI remains in sync with the underlying data.

Suppose you want to build a counter component that increments or decrements a value each time a button is clicked. With TypeScript, you can ensure that the state of the counter is strictly managed, preventing any unintended behavior.

Start by defining the component’s class and properties in your my-component.ts file:

import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('counter-component')
export class CounterComponent extends LitElement {
  @property({ type: Number }) count = 0;

  static styles = css`
    :host {
      display: block;
      padding: 16px;
      background-color: #e0f7fa;
      text-align: center;
      font-family: Arial, sans-serif;
      border-radius: 8px;
    }
    button {
      padding: 8px 16px;
      margin: 4px;
      font-size: 16px;
    }
  `;

  render() {
    return html`
      <h2>Count: ${this.count}</h2>
      <button @click="${this.increment}">Increment</button>
      <button @click="${this.decrement}">Decrement</button>
    `;
  }

  increment() {
    this.count += 1;
  }

  decrement() {
    this.count -= 1;
  }
}

This component introduces two new methods: increment and decrement, which directly modify the count property. Whenever the count changes, the component automatically re-renders, displaying the updated value.

Enhancing User Interactions

Handling user interactions efficiently is key to creating a responsive and engaging web application. In the counter example, button clicks are handled by attaching event listeners directly to the buttons in the template.

TypeScript’s type system ensures that the click events are handled correctly, reducing the risk of runtime errors.

Consider expanding the functionality of the counter component by adding input validation. For instance, you might want to prevent the counter from going below zero. TypeScript makes it straightforward to enforce such constraints, making your components more robust.

Update the decrement method to include a condition that checks the current count:

decrement() {
  if (this.count > 0) {
    this.count -= 1;
  }
}

Now, the counter will not decrement below zero, ensuring that the component behaves predictably. This kind of type-safe logic, backed by TypeScript, helps maintain the integrity of your application, especially as it grows in complexity.

Managing Component Lifecycles

Understanding and managing the lifecycle of your web components is crucial, particularly when dealing with more complex interactions or when integrating with external APIs or services.

Web components have several lifecycle callbacks, such as connectedCallback and disconnectedCallback, which allow you to execute code at specific points in the component’s lifecycle.

For example, if you need to perform setup tasks when the component is added to the DOM, such as fetching data from an API or initializing third-party libraries, you can override the connectedCallback method.

Similarly, if you need to clean up resources or event listeners when the component is removed from the DOM, you can use disconnectedCallback.

Here’s how you might use these lifecycle callbacks in a TypeScript-based web component:

connectedCallback() {
  super.connectedCallback();
  console.log('Component added to the DOM');
  // Initialize external resources or fetch data here
}

disconnectedCallback() {
  super.disconnectedCallback();
  console.log('Component removed from the DOM');
  // Clean up resources or event listeners here
}

These lifecycle methods provide hooks into the creation and destruction process of your components, enabling you to manage resources effectively and ensure that your application remains performant and free of memory leaks.

Advanced State Management Techniques

As your web components become more sophisticated, you may need to manage more complex state or coordinate state across multiple components.

TypeScript’s strong typing and interfaces can be particularly useful in these scenarios, allowing you to define clear contracts for your state objects and ensuring that your components interact with state in a predictable and type-safe manner.

For instance, if you have a component that displays a list of items fetched from an API, you can define an interface for the item data and use TypeScript’s type annotations to ensure that your component handles the data correctly:

interface Item {
  id: number;
  name: string;
}

@customElement('item-list')
export class ItemList extends LitElement {
  @property({ type: Array }) items: Item[] = [];

  // Fetch items and update the state
  async fetchItems() {
    const response = await fetch('/api/items');
    this.items = await response.json();
  }

  render() {
    return html`
      <ul>
        ${this.items.map(item => html`<li>${item.name}</li>`)}
      </ul>
    `;
  }
}

In this example, TypeScript ensures that the items property is always an array of Item objects, preventing potential errors that could arise from handling unexpected data types.

This approach not only improves the reliability of your code but also makes it easier to reason about how state is managed and propagated within your components.

Using TypeScript to manage state and interactivity in web components allows you to build more robust, maintainable, and scalable applications.

By leveraging TypeScript’s type system and features, you can ensure that your components behave predictably and integrate seamlessly with the rest of your application, even as it grows in complexity.

Integrating Web Components with TypeScript in Larger Applications

As you become more comfortable with building individual web components using TypeScript, the next step is to explore how these components can be effectively integrated into larger applications.

Whether you’re working within an existing framework or building a standalone application, understanding how to manage dependencies, organize your code, and ensure smooth interactions between components is crucial for creating maintainable and scalable web applications.

Organizing and Structuring Your Codebase

When developing larger applications, organizing your codebase effectively becomes essential to maintain clarity and prevent technical debt. With TypeScript and web components, you should aim to structure your code in a way that promotes reusability and separation of concerns.

A common approach is to organize your components into a directory structure that reflects their function and relationship within the application. For example, you might have a components directory where each component resides in its own folder, containing the TypeScript file, associated styles, and any other related assets.

Here’s an example of a possible directory structure for a medium-sized application:

src/
├── components/
│   ├── header/
│   │   ├── header-component.ts
│   │   ├── header-component.css
│   ├── footer/
│   │   ├── footer-component.ts
│   └── shared/
│       ├── button-component.ts
├── services/
│   ├── api-service.ts
├── styles/
│   ├── global.css
└── index.ts

In this structure:

  • components/ contains all your web components, organized by feature or common usage. Components that are shared across the application, such as buttons, can reside in a shared/ directory.
  • services/ houses your TypeScript files that handle external API calls, data management, or other non-UI logic.
  • styles/ can include global styles or theme files that apply across the application.
  • index.ts serves as the entry point for your application, where components are registered, and initial logic is handled.

This structure ensures that each component and service is modular, making your codebase more maintainable as the application grows.

Integrating Web Components into Existing Frameworks

Web components, being framework-agnostic, can be seamlessly integrated into existing applications built with popular frameworks like React, Angular, or Vue.js. TypeScript plays a pivotal role in ensuring that these integrations are smooth and type-safe.

For example, if you’re working within a React application, you can easily integrate a web component built with TypeScript. React provides a mechanism to work with custom elements, and TypeScript ensures that the interactions between React and the web component are predictable and error-free.

Here’s how you might use a custom web component within a React application:

import React from 'react';
import './App.css';
import 'path-to-your-web-component';

function App() {
  return (
    <div className="App">
      <h1>React with Web Components</h1>
      <my-component name="React"></my-component>
    </div>
  );
}

export default App;

In this example, my-component is a custom element created with TypeScript. React treats it like any other HTML element, making it easy to integrate into your React components. TypeScript ensures that if the name property expects a string, the correct type is enforced throughout the application.

When integrating with Angular, web components are also straightforward to use. Angular has excellent support for custom elements, and TypeScript’s strict typing ensures that any properties or events defined on your web components are correctly handled.

To use a custom element in Angular:

import { Component } from '@angular/core';
import 'path-to-your-web-component';

@Component({
  selector: 'app-root',
  template: `
    <h1>Angular with Web Components</h1>
    <my-component name="Angular"></my-component>
  `,
})
export class AppComponent {}

Here, Angular recognizes my-component as a custom element, and TypeScript manages the types and interfaces, ensuring that your application remains type-safe and easy to maintain.

Dependency Injection and Service Integration

In larger applications, managing dependencies and integrating services is a common challenge. TypeScript, combined with the modular nature of web components, offers a powerful way to handle these concerns through dependency injection and service classes.

For instance, if your web component needs to fetch data from an external API, you can create a service class that handles all API interactions. This service can then be injected into your web components, ensuring that the logic is decoupled and easily testable.

Here’s an example of how you might set up a service in TypeScript:

export class ApiService {
  async fetchData(url: string): Promise<any> {
    const response = await fetch(url);
    return response.json();
  }
}

You can then use this service in your web component:

import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { ApiService } from '../services/api-service';

@customElement('data-component')
export class DataComponent extends LitElement {
  @property({ type: Array }) data: any[] = [];
  private apiService = new ApiService();

  static styles = css`
    :host {
      display: block;
      padding: 16px;
    }
  `;

  async connectedCallback() {
    super.connectedCallback();
    this.data = await this.apiService.fetchData('/api/data-endpoint');
  }

  render() {
    return html`
      <ul>
        ${this.data.map(item => html`<li>${item.name}</li>`)}
      </ul>
    `;
  }
}

This approach ensures that your data-fetching logic is encapsulated within a service, making it reusable across multiple components. TypeScript’s static typing guarantees that any changes to the service interface are immediately reflected in the components that use it, reducing the risk of runtime errors.

Testing Web Components with TypeScript

Testing is an essential part of building reliable applications, and TypeScript can enhance the testing process by catching potential issues before they become bugs.

When testing web components built with TypeScript, you can use frameworks like Jest or Karma, which offer robust support for TypeScript and web components.

For example, to test the DataComponent, you could write a test that verifies the component’s behavior when fetching data:

import { fixture, html } from '@open-wc/testing-helpers';
import { DataComponent } from '../src/components/data-component';

describe('DataComponent', () => {
  it('fetches and displays data', async () => {
    const element = await fixture<DataComponent>(html`<data-component></data-component>`);
    await element.updateComplete;

    const items = element.shadowRoot?.querySelectorAll('li');
    expect(items?.length).toBeGreaterThan(0);
  });
});

This test uses @open-wc/testing-helpers, a library specifically designed for testing web components. TypeScript’s type definitions ensure that your test code is type-safe, reducing the likelihood of errors.

By following these practices, you can effectively integrate web components into larger applications, ensuring that they are well-organized, maintainable, and easy to scale. TypeScript’s strong typing and modular approach not only enhance the development process but also contribute to the long-term success and reliability of your application.

Conclusion

Using web components with TypeScript offers a powerful approach to building modern, scalable web applications. By combining the modularity and reusability of web components with the type safety and enhanced tooling provided by TypeScript, developers can create robust, maintainable, and efficient applications. Whether integrating with existing frameworks or building standalone applications, TypeScript ensures that your code is reliable, easier to debug, and scalable as your project grows. As web development continues to evolve, mastering the use of web components with TypeScript will be an essential skill for creating high-quality, future-proof web experiences that meet the demands of today’s users.

Read Next: