JS F/k
5 subscribers
3 photos
28 links
HTML/TS/Vue — с примерами, по делу, без воды

https://js-f-k.netlify.app

#html #vue #typescript #npm
Download Telegram
Улучшение дефолтного поведения TypeScript

ts-reset — утилита, которая расширяет стандартную типизацию в TypeScript, устраняя устаревшие и нестрогие участки в базовых API.
Подключается на уровне проекта и повышает строгость типизации в ряде API.

Установка:
npm install --save-dev ts-reset

// reset.d.ts
import "@total-typescript/ts-reset";


Также возможно установить отдельные правила:
// reset.d.ts
import "@total-typescript/ts-reset/json-parse";
import "@total-typescript/ts-reset/fetch";


Основные изменения:

JSON.parse, .json(), localStorage, sessionStorage теперь возвращают unknown, а не any
.filter(Boolean) корректно удаляет falsy-значения
.includes(), .indexOf(), Set.has(), Map.has() не требуют точного сравнения (чего и пытаемся добиться, вызывая их)
Array.isArray() больше не считает any[] безопасным

⚠️ Не рекомендуется использовать в библиотеках, так как изменения глобальных типов могут повлиять на конечные проекты, в которые библиотека будет установлена.

Внутри существующих проектов подключение ts-reset, как правило, не вызывает проблем: типы становятся строже, но остаются совместимыми с корректным кодом.
Если после подключения появляются ошибки — скорее всего, это участки, где типизация и так была небезопасной.

#npm #typescript
Типизация querySelector

typed-query-selector — улучшение типизации методов querySelector и querySelectorAll с выводом типов на основе CSS-селекторов.

⚠️ Требуется TypeScript версии 4.1 или выше.

Установка:
npm install --save-dev typed-query-selector

// typed-query-selector.d.ts
import "typed-query-selector";


Результат:
document.querySelector("div#app"); // HTMLDivElement
document.querySelector("div#app > form#login"); // HTMLFormElement
document.querySelectorAll("span.badge"); // NodeListOf<HTMLSpanElement>
document.querySelector("button#submit"); // HTMLButtonElement

#npm #typescript
Pinia: запрет неявной мутации

Одна из главных причин использовать TypeScript — это предсказуемость.
Любое изменение данных должно быть намеренным: мы либо вызываем функцию, либо обновляем ref.
Но в случае с Pinia возможен такой код:
const store = useDummyStore()
store.stateValue = null // прямая мутация state, но нет ошибки


Явная vs неявная мутация

Что здесь не так?

- store.stateValue = null — выглядит как поле объекта, но на самом деле мутирует глобальное хранилище напрямую.
- storeToRefs(store).stateValue.value = null — требует предварительного шага, вызов storeToRefs, и работы с .value, что делает намерение очевидным.

Такое поведение ближе к commit() из Vuex: мы не просто что-то присваиваем, мы просим хранилище редактировать свой state.
Именно эта намеренность и отделяет хорошую архитектуру от хрупкой.

Pinia и неявные мутации
В отличие от Vuex, Pinia по умолчанию позволяет напрямую менять state.
Нет ни mutations, ни встроенного запрета.
strict режим обсуждался ещё в 2020, но так и остался не реализован.

Как запретить неявные мутации
Мы можем вернуть контроль сами через типизацию. Делаем state доступным только для чтения:
// env.d.ts
declare module 'pinia' {
export interface StoreDefinition<
Id extends string = string,
S extends StateTree = StateTree,
G = _GettersTree<S>,
A = _ActionsTree
> {
(pinia?: Pinia | null | undefined, hot?: StoreGeneric): Store<Id, Readonly<S>, G, A>
}
}


Теперь:
// Прямое присваивание
const store = useDummyStore()
store.stateValue = null // Error: Cannot assign to 'stateValue' because it is a read-only property
// Посредством storeToRefs
const { stateValue } = storeToRefs(useDummyStore())
stateValue.value = null //
// Посредством action
store.setValue(null) //


Pinia предлагает гибкость, но вместе с ней — уязвимость.
Если вы хотите, чтобы изменения state всегда были преднамеренными и контролируемыми, настройка Readonly<StateTree> — хорошая отправная точка.
Мы не ограничиваем возможности Pinia, мы лишь возвращаем понятную дисциплину, которая особенно нужна в команде или на долгосрочном проекте.

#pinia #typescript
Типизированные customEvents

Наверно каждый разработчик хоть раз писал свой 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 и импортированные типы: что может пойти не так?

Когда вы используете 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
Запуск TypeScript напрямую в Node.js

Хорошие новости: в новых версиях Node.js можно запускать .ts-файлы напрямую — без ts-node, tsx или ручной сборки.

v22.6.0: Появилась базовая поддержка type-stripping
v22.7.0: Добавлен флаг --experimental-transform-types
v23.6.0: Удаление аннотации типов включено по умолчанию
v24.3.0: Функция больше не считается экспериментальной

Подробнее о нативной поддержке TypeScript в Node.js — в официальной документации.

Что изменилось
- Node.js сам удаляет аннотации типов (type stripping), оставляя чистый JavaScript.
- Флаг --experimental-strip-types теперь не обязателен — он активен по умолчанию. При необходимости его можно отключить с помощью --no-experimental-strip-types.
- Если используются конструкции, требующие транспиляции (enum, namespace), потребуется флаг --experimental-transform-types.

Пример запуска
node --experimental-transform-types index.ts


Как писать совместимый код
В TypeScript 5.8 появился новый флаг erasableSyntaxOnly. Он запрещает конструкции, которые Node.js не сможет вырезать. Добавив этот параметр в tsconfig.json, редактор предупредит вас о неподдерживаемом коде.

Кому это пригодится?
Крупные проекты на TypeScript, которые активно используют namespace, enum и прочие трансформируемые конструкции, всё равно останутся на привычных инструментах вроде ts-node, tsx или полноценной сборки через tsc — и это нормально. Но для маленьких утилит, тестовых скриптов и инструментов возможность просто запустить node script.ts — отличный способ сэкономить время и не настраивать дополнительное окружение.

#build #typescript