Как стать автором
Обновить

Vue 3 и jsx — неочевидные нюансы типизации

Уровень сложностиПростой
Время на прочтение4 мин
Количество просмотров2.5K
Картинка скомунизжена с этой статьи
Картинка скомунизжена с этой статьи

Всем привет.
Статья представляет из себя краткое резюме из моей практики описания Vue компонентов с помощью полноценных jsx шаблонов, то есть, tsx модулей, возвращающих defineComponent.

Как мы все знаем внешний API компонента во Vue разделен, и имеет не только props. Помимо них есть ещё slots и emitters.

основа defineComponent - функция setup. И как вариант всё можно типизировать через хуки defineProps, defineSlots и defineEmits в теле этой функции. Описывать такой способ типизации я не буду - о нём можно почитать и в документации.

Почему я так не делаю? Я просто не хочу захламлять тело setup функции. Мне нравится отделять логику компонента от его внешнего API и декларировать API через свойства defineComponents, отделяя так сказать мух от котлет. И вот тут то как раз и появляются неочевидные моменты, которые я решил немного поанализировав исходники и погуглив.

Типизация свойства props

import { defineComponent, type PropType } from "vue"
type Option = { id: string, value: string }

export default defineComponent({
  props: {
    propString: {
      type: String as PropType<string>
    },
    propObject: {
      type: Object as PropType<Option>
    },
    propArray: {
      type: Array as PropType<Option[]>
    },
    combinedTypesProp: {
      type: [Object, Array] as PropType<Option | Option[]>
    }
  }
  setup(props) {
    ...
  }
})

Если вы делаете библиотеку компонентов в виде npm пакета, для экспорта типов свойств компонента их нужно выделить в отдельную переменную и из неё экспортировать тип свойств с помощью утилитарного типа ExtractPublicPropTypes

const componentProps = {
  propString: {
    type: String as PropType<string>
  }
}

export type ComponentProps = ExtractPublicPropTypes<typeof componentProps>

export default defineComponent({
  props: componentProps,
  setup(props) {
    ...
  }
})

Типизация свойства emits

const componentEmitters: {
  change: (value: string) => void
  ['item:change']: (value: string) => void
} = {
 /** после объявления типов Vue попросит добавить в сам объект
   объявленные в типах функции. Их добавление обязательно,
   по сути они являются валидаторами эмиттеров, то есть должны вернуть boolean.
   Если вы не хотите валидировать эмиттинг, просто верните true
 */
  change: () => true
  ['item:change']: (value) => typeof value === 'string'
}

export default defineComponent({
  emits: componentEmitters
  setup(_, { emits }) {
    /**
     поскольку эмиттеры вызываются через общую функцию,
     её тип будет отличаться от типа, объявленного переменной componentEmitters.
     он наследует его типы, в данному случае преобразуясь в:
     ((event: "change", value: string) => void) &
     ((event: "item:change", value: string) => void)
    */
  }
})

К сожалению среди модулей Vue я не нашёл утилитарный тип, преобразующий типизацию объекта componentEmitters в типизацию функции emits. Если у вас есть какая-либо информация об этом, сообщите в комментариях. Использовать тип emits может быть удобно, если вы составляете иерархию компонентов и используете provide / inject, чтобы вызывать emit родителя в дочках при определённых кейсах. Они не редки. Например таб в составе табов или спойлер в составе аккордеона или радио в составе радиогруппы.

Типизация slots

Вот тут есть одна маленькая неприятность, полагаю, это недоработка системы типизации компонентов именно для tsx, но не исключено если я рукожоп и что-то делаю не так. Если вы знаете в чём именно проблема и как её можно решить - прошу написать в комментарии.

Типизация слота, при использовании именно в jsx иерархии, действует в рамках компонента. При использовании компонента в родителе, в нём уже не будет наследоваться тип скоупа данных, а функция слота (чем и является слот в vue jsx) будет иметь один и тот же тип: () => JSX.Element . Это проверено на сборке vite с плагином @vitejs/plugin-vue-jsx.
С последующим использованием компонентов с такой типизацией в однофайловых компонентах такой проблемы нет - типы скоупа успешно применяются.

import { type SlotsType } from "vue"



type ScopeOne = {...}
type ScopeTwo = {...}
const componentSlots: SlotsType<{
  default: () => VNodeChild | undefined
  namedSlotOne: (props: ScopeOne) => VNodeChild | undefined
  namedSlotTwo: (props: ScopeTwo) => VNodeChild | undefined
}> = {}

export default defineComponent({
  slots: componentSlots,
  setup(_, { slots }) {
    /**
     тип слота переедет из SlotsType<T> в ReadOnly<T>
    */
  }
})

Послесловие

Статью я хочу завершить несколькими важными тезисами и оставить пару ссылок.

JSX в последнее время стал своего рода стандартом благодаря одной небезызвестной библиотеке, захватившей рынок. Чьё название я не хочу называть тк от него уже глаз дёргается. Поэтому прошу вас не давать оценочных суждений - что лучше, vue template compiler или jsx. Оба варианта одинаково гибкие, каждый имеет свои нюансы. Поэтому применять или не применять jsx во vue - вопрос вкуса.

Разработчики Vue понимают этот стандарт, поэтому со скорым релизом Vapor уже готовится vite plugin для применения jsx под Vapor. За процессом разработки плагина можно понаблюдать тут. Автор плагина поделился со мной ссылкой на dev пакет с поддержкой Vapor, поэтому особо любознательным поиграться можно уже сейчас.

Всех девушек поздравляю с наступающим праздником и желаю вам всем успехов, как в личной, так и в профессиональной сфере! =)

Теги:
Хабы:
Всего голосов 4: ↑3 и ↓1+3
Комментарии4

Публикации

Работа

Ближайшие события