Типизация компонентов

Haunted — это библиотека для создания веб-компонентов с использованием реактивного подхода и хуков, построенная поверх стандартных Web Components. Одной из ключевых задач при работе с Haunted является корректная типизация компонентов, которая обеспечивает предсказуемое поведение, улучшает читаемость кода и снижает количество ошибок на этапе компиляции.


Типы компонентов

В Haunted компоненты создаются с помощью функции component. Тип компонента определяется через параметры, передаваемые этой функции:

import { component, html } from 'haunted';

const MyComponent = component((props) => html`<p>${props.name}</p>`);

Ключевой момент: props — это объект с набором свойств, которые передаются компоненту. Их можно типизировать с помощью TypeScript для строгой проверки:

interface MyComponentProps {
  name: string;
  age?: number; // необязательное свойство
}

const MyComponent = component<MyComponentProps>((props) => html`
  <p>${props.name}, ${props.age ?? 'Возраст не указан'}</p>
`);

Использование интерфейсов позволяет сразу видеть, какие свойства обязательны, а какие опциональны, и получать автокомплит в редакторе.


Типизация хуков

Haunted поддерживает стандартные хуки, аналогичные React: useState, useEffect, useReducer и другие. Для строгой типизации состояния или возвращаемых значений хуков можно использовать дженерики:

import { useState } from 'haunted';

const Counter = component(() => {
  const [count, setCount] = useState<number>(0);

  return html`<button @click=${() => setCount(count + 1)}>Счетчик: ${count}</button>`;
});

Особенности:

  • useState<T> позволяет указать тип состояния.
  • useReducer<S, A> позволяет определить тип состояния S и тип действий A.
  • Для useRef<T> можно задавать тип элементов DOM или пользовательских объектов.

Типизация событий

События в Haunted могут быть кастомными, что делает их важной частью типизации. Для типизации события используют интерфейсы с полями detail и type:

interface IncrementEventDetail {
  step: number;
}

const Counter = component(() => {
  const handleClick = (e: CustomEvent<IncrementEventDetail>) => {
    console.log(e.detail.step);
  };

  return html`<button @click=${handleClick}>Увеличить</button>`;
});

Совет: При генерации событий через dispatchEvent следует явно указывать тип события:

const incrementEvent: CustomEvent<IncrementEventDetail> = new CustomEvent('increment', { detail: { step: 1 } });
element.dispatchEvent(incrementEvent);

Это гарантирует согласованность типов между компонентом и его пользователями.


Типизация слотов

Haunted поддерживает стандартные Web Components слоты. Для строгой типизации содержимого слотов можно использовать интерфейсы и дженерики:

interface SlotProps {
  header?: HTMLElement;
  footer?: HTMLElement;
}

const Layout = component<SlotProps>((props) => html`
  <header><slot name="header">${props.header}</slot></header>
  <main><slot></slot></main>
  <footer><slot name="footer">${props.footer}</slot></footer>
`);

Слоты могут быть динамическими, поэтому важно указывать допустимые типы контента, чтобы избежать ошибок при доступе к ним из кода компонента.


Типизация ref и элементов DOM

Для доступа к элементам DOM в Haunted используется useRef или ref атрибуты. Типизация позволяет работать с конкретными элементами безопасно:

import { useRef } from 'haunted';

const InputComponent = component(() => {
  const inputRef = useRef<HTMLInputElement>(null);

  const focusInput = () => {
    inputRef.current?.focus();
  };

  return html`<input ${ref(inputRef)} type="text" /><button @click=${focusInput}>Фокус</button>`;
});

Ключевой момент: использование <HTMLInputElement> вместо any гарантирует корректное обращение к свойствам элемента, таким как .value, .focus() и другим.


Дженерики для компонентов

Haunted позволяет использовать дженерики для создания компонентов с гибкой типизацией. Это особенно полезно для переиспользуемых библиотечных компонентов:

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => string;
}

const List = component<ListProps<any>>((props) => html`
  <ul>
    ${props.items.map(item => html`<li>${props.renderItem(item)}</li>`)}
  </ul>
`);

С дженериками можно создавать универсальные компоненты:

const NumberList = component<ListProps<number>>((props) => html`
  <ul>${props.items.map(item => html`<li>${item}</li>`)}</ul>
`);

Интеграция с TypeScript

Полная интеграция Haunted с TypeScript позволяет:

  • Получать автодополнение для props, state, refs.
  • Обнаруживать несоответствие типов при вызове компонента.
  • Обеспечивать безопасное использование хуков и событий.
  • Создавать универсальные и переиспользуемые компоненты с дженериками.

Пример: строгая типизация компонента с состоянием, событиями и слотом:

interface CardProps {
  title: string;
  onClose: (e: CustomEvent<void>) => void;
}

const Card = component<CardProps>((props) => {
  const [visible, setVisible] = useState<boolean>(true);

  const closeCard = () => {
    setVisible(false);
    props.onClose(new CustomEvent('close'));
  };

  return visible ? html`
    <div class="card">
      <header><slot name="title">${props.title}</slot></header>
      <button @click=${closeCard}>Закрыть</button>
    </div>
  ` : null;
});

Типизация в Haunted охватывает все основные аспекты: свойства, состояние, события, слоты и ссылки на DOM. Она обеспечивает строгую проверку на этапе компиляции и делает компоненты безопасными и предсказуемыми в работе. Правильная типизация повышает читаемость кода и упрощает поддержку сложных приложений.