Улучшение дефолтного поведения TypeScript
ts-reset — утилита, которая расширяет стандартную типизацию в TypeScript, устраняя устаревшие и нестрогие участки в базовых API.
Подключается на уровне проекта и повышает строгость типизации в ряде API.
Установка:
Также возможно установить отдельные правила:
Основные изменения:
•
•
•
•
⚠️ Не рекомендуется использовать в библиотеках, так как изменения глобальных типов могут повлиять на конечные проекты, в которые библиотека будет установлена.
Внутри существующих проектов подключение
Если после подключения появляются ошибки — скорее всего, это участки, где типизация и так была небезопасной.
#npm #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
Total TypeScript
TS Reset - Official Docs
Learn how to use TypeScript to level-up your applications as a web developer through exercise driven self-paced workshops and tutorials hosted by TypeScript wizard Matt Pocock.
Типизация querySelector
typed-query-selector — улучшение типизации методов
⚠️ Требуется TypeScript версии 4.1 или выше.
Установка:
Результат:
#npm #typescript
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
npm
npm: typed-query-selector
Better typed `querySelector` and `querySelectorAll`.. Latest version: 2.12.0, last published: a year ago. Start using typed-query-selector in your project by running `npm i typed-query-selector`. There are 29 other projects in the npm registry using typed…
Pinia: запрет неявной мутации
Одна из главных причин использовать TypeScript — это предсказуемость.
Любое изменение данных должно быть намеренным: мы либо вызываем функцию, либо обновляем
Но в случае с Pinia возможен такой код:
Явная vs неявная мутация
Что здесь не так?
- ❌
- ✅
Такое поведение ближе к
Именно эта намеренность и отделяет хорошую архитектуру от хрупкой.
Pinia и неявные мутации
В отличие от Vuex, Pinia по умолчанию позволяет напрямую менять
Нет ни
strict режим обсуждался ещё в 2020, но так и остался не реализован.
Как запретить неявные мутации
Мы можем вернуть контроль сами через типизацию. Делаем
Теперь:
Pinia предлагает гибкость, но вместе с ней — уязвимость.
Если вы хотите, чтобы изменения
Мы не ограничиваем возможности Pinia, мы лишь возвращаем понятную дисциплину, которая особенно нужна в команде или на долгосрочном проекте.
#pinia #typescript
Одна из главных причин использовать 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-сервис.
Вот и мы написали свой. Но хотя бы типизированный.
Мы используем
- обеспечить строгую типизацию названий событий и их
- иметь автодополнение при вызове событий
- заранее знать структуру
Как использовать:
Реализация:
Если вы обходитесь без глобального стора, не хотите лишних зависимостей и при этом цените строгую типизацию — возможно, вам подойдёт именно такой вариант.
#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
Запуск TypeScript напрямую в Node.js
Хорошие новости: в новых версиях Node.js можно запускать
v22.6.0: Появилась базовая поддержка
v22.7.0: Добавлен флаг
v23.6.0: Удаление аннотации типов включено по умолчанию
v24.3.0: Функция больше не считается экспериментальной
Подробнее о нативной поддержке TypeScript в Node.js — в официальной документации.
Что изменилось
- Node.js сам удаляет аннотации типов (type stripping), оставляя чистый JavaScript.
- Флаг
- Если используются конструкции, требующие транспиляции (
Пример запуска
Как писать совместимый код
В TypeScript 5.8 появился новый флаг
Кому это пригодится?
Крупные проекты на TypeScript, которые активно используют
#build #typescript
Хорошие новости: в новых версиях 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
nodejs.org
Node.js — Running TypeScript Natively
Node.js® is a free, open-source, cross-platform JavaScript runtime environment that lets developers create servers, web apps, command line tools and scripts.