Типизированные customEvents
Наверно каждый разработчик хоть раз писал свой CustomEvent-сервис.
Вот и мы написали свой. Но хотя бы типизированный.
Мы используем
- обеспечить строгую типизацию названий событий и их
- иметь автодополнение при вызове событий
- заранее знать структуру
Как использовать:
Реализация:
Если вы обходитесь без глобального стора, не хотите лишних зависимостей и при этом цените строгую типизацию — возможно, вам подойдёт именно такой вариант.
#typescript #vue #vueuse
Наверно каждый разработчик хоть раз писал свой CustomEvent-сервис.
Вот и мы написали свой. Но хотя бы типизированный.
Мы используем
window.dispatchEvent()
и useEventListener()
из VueUse, но оборачиваем их, чтобы:- обеспечить строгую типизацию названий событий и их
payload
- иметь автодополнение при вызове событий
- заранее знать структуру
event.detail
без явных проверок и any
Как использовать:
dispatchCustomDashboardEvent("user-created", user.id);
useDashboardEventListener("user-created", ({ detail }) => {
state = detail; // string, не any
});
Реализация:
import { useEventListener, type Arrayable } from "@vueuse/core";
// Доступные события и payload описываются через интерфейс
export interface CustomDashboardEvent {
"user-created": User["id"]
"user-deleted": never
}
type DashboardEventName = keyof CustomDashboardEvent;
// Создание типизированного события
function createCustomDashboardEvent<EventName extends DashboardEventName>(
eventName: EventName,
payload?: CustomDashboardEvent[EventName]
): CustomEvent<CustomDashboardEvent[EventName]> {
return new CustomEvent<CustomDashboardEvent[EventName]>(eventName, { detail: payload });
}
// Вызов события
export function dispatchCustomDashboardEvent<EventName extends DashboardEventName>(
eventName: EventName,
...[payload]: CustomDashboardEvent[EventName] extends never ? [] : [CustomDashboardEvent[EventName]]
): void {
window.dispatchEvent(createCustomDashboardEvent(eventName, payload));
}
// Прослушивание событий
export function useDashboardEventListener<EventName extends DashboardEventName>(
eventName: Arrayable<EventName>,
callback: (eventData: CustomEventInit<CustomDashboardEvent[EventName]>) => void,
options?: AddEventListenerOptions
): void {
useEventListener(window, eventName, callback, options);
}
Если вы обходитесь без глобального стора, не хотите лишних зависимостей и при этом цените строгую типизацию — возможно, вам подойдёт именно такой вариант.
#typescript #vue #vueuse
Vue, TypeScript и импортированные типы: что может пойти не так?
Когда вы используете
Компилятор постарается сделать всё возможное, чтобы вывести эквивалентные параметры времени выполнения, основанные на аргументах типа. Но сработает это не всегда.
Проблема: неработающий Boolean при импорте
На момент
Пример
Результат
Как видно из примера — если не передавать в проп
Решение
Если поменять тип на локально объявленный, всё работает верно.
В таком случае TypeScript передаёт в Vue AST более полную информацию о локальных типах, и Vue корректно использует конструктор
Проверить самостоятельно
Если хотите поэкспериментировать: Открыть демо на Vue Playground
Итог
Если вы столкнулись с тем, что
Если вам известно другое решение — пишите, будет интересно разобраться!
#typescript #vue #unresolved
Когда вы используете
defineProps
в Vue с TypeScript, компилятор Vue запускает обратную генерацию типов: из интерфейсов, аннотаций и значений по умолчанию (default
) создаются JS-конструкторы — Boolean
, String
, Object
и другие. Именно они потом используются в скомпилированном компоненте в рантайме.Компилятор постарается сделать всё возможное, чтобы вывести эквивалентные параметры времени выполнения, основанные на аргументах типа. Но сработает это не всегда.
Проблема: неработающий Boolean при импорте
На момент
vue@3.5.*
, если типы пропов импортируются, а не определяются прямо в компоненте — Vue может некорректно вывести тип Boolean
при использовании shorthand
-синтаксиса.Пример
<!-- Comp.vue -->
<script setup lang="ts">
import { Props } from "./types";
withDefaults(defineProps<Props>(), {
disabled: false,
sampleBooleanProp: false
});
</script>
// types.ts
import { InputHTMLAttributes } from "vue";
export interface Props {
disabled?: InputHTMLAttributes["disabled"] // импортированный тип
sampleBooleanProp?: boolean
}
Результат
<!-- Пропы не указаны. Должны примениться значения по умолчанию -->
<Comp/>
<!-- ✅ Работает верно. disabled: false, sampleBooleanProp: false -->
<!-- Пропы указаны со значениями. Должны примениться значения true -->
<Comp :disabled="true" :sampleBooleanProp="true"/>
<!-- ✅ Работает верно. disabled: true, sampleBooleanProp: true -->
<!-- Пропы указаны со значениями. Должны примениться значения false -->
<Comp disabled="false" sampleBooleanProp="false"/>
<!-- ✅ Работает верно. disabled: false, sampleBooleanProp: false -->
<!-- Пропы указаны в shorthand-формате. Должны примениться значения true -->
<Comp disabled sampleBooleanProp/>
<!-- ❌ Ошибка. disabled: '' (пустая строка), sampleBooleanProp: true -->
Как видно из примера — если не передавать в проп
disabled
значение, то вместо ожидаемого true
, проп равен пустой строке. То есть Vue не сгенерировал конструктор Boolean
для disabled
— он не смог вывести правильный тип из импортированного интерфейса.Решение
Если поменять тип на локально объявленный, всё работает верно.
// types.ts
// локальный интерфейс, полностью идентичен InputHTMLAttributes
interface InputHTMLAttributes_Local { disabled: boolean | "true" | "false" }
export interface Props {
disabled?: InputHTMLAttributes_Local["disabled"] // локальный аналог
sampleBooleanProp?: boolean
}
В таком случае TypeScript передаёт в Vue AST более полную информацию о локальных типах, и Vue корректно использует конструктор
Boolean
.Проверить самостоятельно
Если хотите поэкспериментировать: Открыть демо на Vue Playground
Итог
Если вы столкнулись с тем, что
shorthand
-пропы ведут себя странно — вероятно, проблема в неправильно выведенном типе из импортированного интерфейса. Пока единственное решение — объявить типы локально или отказаться от shorthand
-синтаксиса.Если вам известно другое решение — пишите, будет интересно разобраться!
#typescript #vue #unresolved