Думаю, что сейчас необходимость статической типизации фронтенд приложений не вызывает сомнений, не буду описывать все ее плюсы и минусы, так как на Хабр уже неоднократно обсуждалась данная тема. В данной статье бы хотел предложить решение, которое позволит выполнять проверку типов при создании HTML разметки во Vue.

Какие проблемы существуют?

Типизация - это одна из главных проблем Vue.

В: Каковы самые слабые места Vue?

O: На данный момент, наверное, недружественность к типизации. Наш API разрабатывался без планирования поддержки типизированных языков (типа TypeScript), но мы сделали большие улучшения в 2.5.

из статьи Создатель Vue.js отвечает Хабру

Для типизации скрипта во Vue используется достаточно популярное решение - это библиотека vue-class-component, но вопросы по типизации шаблона так и остаются открытыми.

Какую типизацию для шаблонов хотелось бы получить?

Для удобной работы с шаблонами необходимо, чтобы выполнялись следующие условия:

  • Проверка используемых переменных

  • Проверка атрибутов для HTML элементов

  • Проверка событий(должен определяться тип для объекта события) для HTML элементов

  • Проверка параметров компонента

  • Проверка событий(должен определяться тип для объекта события) компонентов

  • Проверка слотов(должен определяться тип для параметров слота)

Поиск решения

Решений для типизации шаблонов я не нашел, поэтому стал рассматривать другие варианты.

Кроме шаблонов, Vue так же поддерживает render - функции и jsx. TypeScript поддерживает проверку типов для jsx, поэтому я решил подключить его в проект и посмотреть, что из этого получиться.

Подключение jsx позволило решить ряд проблем, но у компонентов не определялись параметры, слоты и события, исправить это мне помогла библиотека vue-tsx-support.

Подключение и использование данной библиотеки достаточно подробно описано у них на странице, поэтому сюда не буду дублировать эту информацию.

События во vue-tsx-support

Единственное, что мне не понравилось в данной библиотеке, так это работа с событиями. Вместо использования this.$emit они предлагают tsx.emitOn

import * as tsx from "vue-tsx-support";

tsx.emitOn(this, "onRowClicked", { item, index });

Проблема типизации событий связана с тем, что в jsx названия событий начинаются с префикса "on" (во vue создаем событие rowClicked, а в jsx подписываемся на onRowClicked)

В функции emitOn происходит преобразование названия события из jsx стиля к стандартному vue стилю("onRowClicked" => "rowClicked")

function emitOn<Events, Name extends string & keyof Events>(
  vm: Vue & { _tsx: DeclareOnEvents<Events> },
  name: Name,
  ...args: Parameters<EventHandler<Events[Name]>>
) {
  vm.$emit(
    name.replace(/^on[A-Z]/, v => v[2].toLowerCase()),
    ...args
  );
}

Решить проблему типизации событий можно и без данной функции. Для этого нужно переопределить типы для Component в библиотеке vue-tsx-support, добавив туда четвертый параметр VueEvents.

declare module "vue-tsx-support/lib/api" {
  interface Component<
    Props,
    PrefixedEvents = {},
    ScopedSlotArgs = {},
    VueEvents = {}
  > extends Vue {
    _tsx: TsxComponentTypeInfo<{}, Props, PrefixedEvents, {}>;
    $scopedSlots: InnerScopedSlots<ScopedSlotArgs>;
    $emit<T extends string & keyof VueEvents>(
      event: T,
      ...args: Parameters<EventHandler<VueEvents[T]>>
    ): this;
  }
}

Во VueEvents будут указываться названия событий в том виде, в котором они используются во vue(события в jsx стиле это PrefixedEvents).

Для того чтобы не делать отдельные интерфейсы для vue событий и jsx событий, я использую тип который выполняет данные преобразования(jsxEventMapper).

type ReverseMap<T extends Record<keyof T, keyof any>> = {
	[P in T[keyof T]]: {
		[K in keyof T]: T[K] extends P ? K : never;
	}[keyof T];
};

type ReverseJsxEvent<T> = ReverseMap<{ [key in keyof T & string]: `on${Capitalize<key>}` }>;

type jsxEventMapper<T> = {
	[rootKey in keyof ReverseJsxEvent<T>]: T[ReverseJsxEvent<T>[rootKey]];
};

В конечном итоге компонент выглядит следующим образом.

import * as tsx from 'vue-tsx-support';
import { VNode } from 'vue';
import Component from 'vue-class-component';
import { jsxEventMapper } from './jsxHelpers';

interface IProps {
	myProp1: string;
  myProp2: string;
}
interface IEvents {
	myEvent: number;
}
interface ISlots {
	mySlot: string;
}

@Component({})
export default class MyComponent extends tsx.Component<
	IProps,
	jsxEventMapper<IEvents>,
	ISlots,
	IEvents
> {
	private render(): VNode {}
}

IntelliSense в Visual Studio Code

Так же хотел бы отметить, что Visual Studio Code, отображает подсказки при работе с такими компонентами, что существенно улучшает процесс разработки.

Комментарии (5)


  1. antyas
    16.02.2022 13:14
    +2

    А еще лучше будет перейти на vue3, где типизация шаблонов из коробки. И еще куча приятных вещей улучшающих DX. По трудозатратам выйдет не больше чем тащить в проект jsx и vue-class-component (причем оба гадость еще та).


    1. SergeyPeskov Автор
      16.02.2022 22:56

      А еще лучше будет перейти на vue3, где типизация шаблонов из коробки

      Разве во Vue 3 есть решена проблема проверки типов в шаблоне?


      1. PGGSMR63
        17.02.2022 10:08

        Да, сам Vue3 переписан на TS, так что там с поддержкой типов все в разы лучше, плюсом к тому Evan уже объявил Vue3 дефолтной версией Vue, так что если планируете дальнейшее развитие проекта, лучше будет его переписать на Vue3.


        1. js_n00b
          17.02.2022 13:09
          +1

          Во Vue 3 действительно с типизацией все гораздо лучше, но вопрос немного не о том. Сам недавно столкнулся с проблемой, когда я поменял имя свойства объекта и его интерфейс, но забыл поменять на новое значение в template. И долго сидел не понимая почему у меня ничего не работает. TS не сообщил об ошибке.


        1. SergeyPeskov Автор
          17.02.2022 13:53

          Да, с типизацией скрипта во Vue 3 проблем быть не должно, но я говорю именно про шаблон(<template></template>), информации о том, что данную проблему решили я не нашел.