JSDoc на практике: как добавить типизацию в JS-проект без TypeScript
.ts файла. Рассмотрим пошаговую настройку окружения, основные приёмы типизации переменных, функций и DOM-элементов, создание собственных типов с @typedef, а также следующие шаги — от автоматической проверки в CI до генерации .d.ts файлов. Все примеры адаптированы для самостоятельного повторения.Введение
В предыдущей статье мы обосновали, почему JSDoc является прагматичной альтернативой TypeScript. Были рассмотрены преимущества подхода, его ограничения и сценарии, в которых отказ от транспиляции даёт ощутимые выгоды.
Теперь перейдём к практической реализации. В качестве примера возьмём реальный проект — UI-кит Kelp, разработанный Крисом Фердинанди. Автор столкнулся с типичной дилеммой: с одной стороны, статическая типизация полезна для поддержки кода, с другой — не хотелось отказываться от чистого JavaScript и усложнять процесс сборки.
Решение было найдено в связке JSDoc и TypeScript Compiler. Рассмотрим, как именно автор настроил проверку типов, какие приёмы использовал и с какими сложностями столкнулся. Все примеры адаптированы для самостоятельного повторения.
Необходимые компоненты
Прежде чем перейти к примерам, уточним, что именно потребуется для реализации подхода, использованного в проекте Kelp.
Компилятор TypeScript
Основной инструмент — компилятор TypeScript (tsc). Это может звучать противоречиво, поскольку мы договорились не использовать TypeScript как язык. Однако компилятор выполняет здесь служебную роль: он отвечает за проверку типов, но не участвует в транспиляции кода.
Установка выполняется стандартным образом:
npm install -D typescriptПосле установки становятся доступны:
- Проверка типов в редакторе — через TypeScript Language Server (встроен в VS Code, доступен как плагин для других редакторов)
- Запуск проверки из командной строки — через команду
npx tscс соответствующими флагами
Конфигурационный файл
Для работы с JavaScript-файлами требуется файл конфигурации — jsconfig.json или tsconfig.json с определёнными настройками. Его назначение — указать компилятору, какие файлы проверять и насколько строго это делать.
В проекте Kelp используется именно такой подход: TypeScript установлен как зависимость, создан минимальный конфигурационный файл, а весь код остаётся в .js файлах с JSDoc-аннотациями.
Среда разработки
Для полноценной работы потребуется редактор с поддержкой TypeScript Language Server:
- VS Code — поддержка включена по умолчанию
- Sublime Text — требуется установка пакетов LSP и LSP-typescript
- WebStorm — поддержка TypeScript встроена, необходимо лишь включить проверку JavaScript-файлов
Все примеры, которые будут рассматриваться далее, работают в любом из перечисленных редакторов при условии корректной настройки.
Что не потребуется
Важно подчеркнуть, что нам не нужны:
- Транспиляция кода в продакшене
- Преобразование
.jsфайлов в.ts - Изучение синтаксиса TypeScript (достаточно знать JSDoc)
- Настройка сложных сборщиков (Webpack, Rollup и т.п.) ради типизации
Весь код остаётся ванильным JavaScript и может выполняться непосредственно в браузере или Node.js без дополнительных шагов.
Начальная настройка окружения
Рассмотрим шаги, которые предпринял Крис Фердинанди для настройки проверки типов в проекте Kelp. Эта последовательность действий универсальна и подойдёт для любого JavaScript-проекта.
Установка TypeScript
Первый шаг — добавление компилятора TypeScript в devDependencies:
npm install -D typescriptУстановка выполняется однократно для проекта. Важно отметить, что TypeScript не становится зависимостью, необходимой для работы приложения в продакшене — он используется только на этапе разработки и в CI-процессах.
Создание конфигурационного файла
В корне проекта создаётся файл jsconfig.json. В проекте Kelp используется следующая конфигурация:
{
"include": [
"src/js/**/*.js"
],
"compilerOptions": {
"target": "es2022",
"allowJs": true,
"checkJs": true,
"strict": true,
"noEmit": true,
"module": "ESNext"
}
}Рассмотрим основные параметры:
| Параметр | Назначение |
|---|---|
include | Указывает, какие файлы подлежат проверке |
checkJs: true | Включает проверку типов для JavaScript-файлов |
allowJs: true | Разрешает обработку JS-файлов (необходимо для работы checkJs) |
strict: true | Включает все строгие проверки типов |
noEmit: true | Запрещает генерацию выходных файлов (нам нужна только проверка) |
target: "es2022" | Задаёт версию ECMAScript для проверки |
В оригинальной конфигурации Крис также добавил ссылку на файл types.d.ts для глобальных типов, но на начальном этапе достаточно базовых настроек.
Проверка работоспособности
После установки и настройки можно убедиться, что всё работает корректно, двумя способами:
В редакторе: Открыть любой JavaScript-файл из проекта и намеренно допустить ошибку типов. Например, присвоить число переменной, которая в JSDoc объявлена как строка. Редактор должен подсветить ошибку.
В терминале: Выполнить команду проверки:
npx tsc --noEmitФлаг --noEmit дублирует опцию из конфигурации и гарантирует, что никаких файлов создано не будет — только проверка.
Добавление скрипта в package.json
Для удобства в package.json добавляется скрипт:
{
"scripts": {
"type-check": "tsc --noEmit"
}
}Теперь проверку можно запускать командой npm run type-check. Это удобно для интеграции с CI/CD, о чём поговорим позже.
Настройки для разных редакторов
VS Code — после создания jsconfig.json всё работает автоматически, дополнительные действия не требуются.
Sublime Text — необходимо установить пакеты:
- LSP (через Package Control)
- LSP-typescript
WebStorm — проверка типов для JSDoc включается в настройках: Languages & Frameworks → JavaScript → TypeScript (убедиться, что опция TypeScript language service включена).
На этом начальная настройка завершена. Проект готов к использованию JSDoc-аннотаций с полноценной проверкой типов.
Типизация переменных и функций
Рассмотрим, как непосредственно добавлять типы в код с помощью JSDoc. В проекте Kelp используется несколько основных тегов, покрывающих большинство повседневных задач.
Типизация переменных
Для объявления типа переменной используется тег @type. Синтаксически он представляет собой JSDoc-комментарий, начинающийся с /**:
/** @type {string} */
const userName = 'Анна';
/** @type {number[]} */
const scores = [95, 87, 92];
/** @type {Array<{id: number, name: string}>} */
const users = [
{ id: 1, name: 'Анна' },
{ id: 2, name: 'Иван' }
];Если переменная может содержать значения разных типов, используется оператор объединения (pipe):
/** @type {HTMLFormElement | null} */
const signinForm = document.querySelector('#sign-in');В проекте Kelp такой подход применяется при работе с DOM-элементами, которые могут отсутствовать на странице.
Типизация функций
Для документирования функций используются теги @param (для параметров) и @returns (для возвращаемого значения):
/**
* Вычисляет сумму двух чисел
* @param {number} a - Первое слагаемое
* @param {number} b - Второе слагаемое
* @returns {number} Сумма a и b
*/
function add(a, b) {
return a + b;
}Если параметр необязательный, это указывается квадратными скобками. Значение по умолчанию можно описать в комментарии или задать непосредственно в коде:
/**
* Приветствует пользователя
* @param {string} name - Имя пользователя
* @param {string} [greeting] - Приветствие (по умолчанию 'Привет')
*/
function sayHello(name, greeting = 'Привет') {
console.log(`${greeting}, ${name}!`);
}В проекте Kelp автор использует JSDoc для всех публичных функций. Например, так выглядит описание метода для работы с toast-уведомлениями:
/**
* Показывает всплывающее уведомление
* @param {string} message - Текст уведомления
* @param {('success'|'error'|'info')} type - Тип уведомления
* @returns {void}
*/
function notify(message, type) {
// реализация
}Типизация колбэков
Для функций, принимающих другие функции в качестве аргументов, можно описать тип колбэка напрямую:
/**
* Фильтрует массив по заданному условию
* @param {number[]} arr - Исходный массив
* @param {(item: number) => boolean} predicate - Функция-предикат
* @returns {number[]} Отфильтрованный массив
*/
function filterArray(arr, predicate) {
return arr.filter(predicate);
}Что даёт такая типизация
После добавления JSDoc-аннотаций редактор начинает:
- Подсвечивать ошибки несоответствия типов (например, попытку передать строку в функцию, ожидающую число)
- Показывать подсказки с типами параметров при вызове функции
- Предоставлять автодополнение с учётом типов
При этом код остаётся полностью рабочим JavaScript и может выполняться без какой-либо компиляции.
Типизация в проекте Kelp: конкретные примеры
В своей статье Крис приводит несколько характерных примеров из своего кода. Вот как выглядит типизация простой функции сложения:
/**
* Складывает два числа
* @param {number} num1
* @param {number} num2
*/
function add(num1, num2 = 0) {
return num1 + num2;
}И типизация коллекции элементов:
/** @type {NodeListOf<HTMLHeadingElement>} */
const headings = document.querySelectorAll('h2, h3, h4, h5, h6');Работа с DOM и обработка событий
Одна из наиболее частых проблем при использовании TypeScript (и JSDoc) — работа с DOM-элементами и событиями. TypeScript Language Server не всегда может определить тип элемента, с которым мы работаем, и начинает сигнализировать об ошибках. Рассмотрим, как эта проблема решается в проекте Kelp.
Типизация DOM-элементов
При получении элемента через querySelector TypeScript не знает точно, какой тип элемента будет возвращён. По умолчанию он предполагает Element | null, что часто недостаточно конкретно:
const searchInput = document.querySelector('.search-input');
// Тип searchInput — Element | null
// Свойство value на Element отсутствует — редактор покажет ошибкуРешение — явно указать тип через JSDoc:
/** @type {HTMLInputElement | null} */
const searchInput = document.querySelector('.search-input');
// Теперь редактор знает о существовании свойства valueВ проекте Kelp такой подход используется повсеместно. Например, при работе с табами:
/** @type {NodeListOf<HTMLButtonElement>} */
const tabButtons = document.querySelectorAll('.tab-button');Проблема с обработчиками событий
Классическая ситуация, описываемая Крисом в статье:
function handleChangeEvent(event) {
// TypeScript сигнализирует об ошибке:
// "Свойства 'checked' не существует на типе 'EventTarget'"
const isChecked = event.target.checked;
}Ошибка возникает потому, что:
eventможет быть любым типом события, не обязательноChangeEventevent.targetимеет типEventTarget, у которого действительно нет свойстваchecked
Решение через проверки instanceof
Первый способ решения — добавить проверки типов непосредственно в код:
function handleChangeEvent(event) {
// Проверяем, что событие — именно ChangeEvent
if (!(event instanceof ChangeEvent)) return;
// Проверяем, что целевой элемент — чекбокс
if (!(event.target instanceof HTMLInputElement)) return;
// Теперь TypeScript понимает, что у event.target есть свойство checked
const isChecked = event.target.checked;
}Этот подход не только решает проблему типизации, но и делает код безопаснее в рантайме.
Приведение типов (type assertion) в JSDoc
Иногда проверки instanceof избыточны или нежелательны (например, когда мы точно знаем тип элемента из контекста). В таких случаях используется приведение типов через JSDoc.
Синтаксис приведения отличается от обычного тега @type: тип указывается в комментарии, а выражение берётся в скобки:
const heading = /** @type {HTMLHeadingElement} */ (document.querySelector('h1'));В TypeScript для этого используются угловые скобки (<HTMLHeadingElement>heading), в JSDoc — конструкция с комментарием и скобками.
В проекте Kelp это используется, например, при работе с коллекцией заголовков:
/** @type {NodeListOf<HTMLHeadingElement>} */
const headings = document.querySelectorAll('h2, h3, h4, h5, h6');
function getNextLevel(index) {
// Приводим элемент к конкретному типу
const nextHeading = /** @type {HTMLHeadingElement} */ (headings[index + 1]);
return nextHeading?.tagName.slice(1) || null;
}Типизация событий в колбэках
Ещё один способ — типизировать параметр функции напрямую:
element.addEventListener('click',
/** @param {MouseEvent} event */
(event) => {
console.log(event.clientX, event.clientY); // Свойства MouseEvent доступны
}
);Это удобно для коротких колбэков, где создание отдельных функций с проверками было бы избыточным.
Общий принцип
При работе с DOM и событиями важно понимать: TypeScript Language Server проверяет код статически, без выполнения. Он не знает, какой именно элемент вернёт querySelector в рантайме. Задача разработчика — предоставить эту информацию через JSDoc или через проверки instanceof, которые одновременно служат и защитой для кода, и подсказкой для системы типов.
Создание пользовательских типов с @typedef
В проектах любого размера рано или поздно возникают повторяющиеся структуры данных. В проекте Kelp, например, это конфигурация компонентов, объекты пользователей и параметры вызова методов. Для таких случаев в JSDoc предусмотрен тег @typedef, позволяющий определять собственные типы.
Базовый синтаксис @typedef
Тег @typedef определяет новый тип, который можно использовать в любом месте проекта так же, как встроенные типы:
/**
* @typedef {Object} User
* @property {number} id - Уникальный идентификатор
* @property {string} name - Имя пользователя
* @property {string} email - Электронная почта
* @property {boolean} [isActive] - Флаг активности (необязательное поле)
*/
/** @type {User} */
const currentUser = {
id: 42,
name: 'Иван Петров',
email: 'ivan@example.com'
// isActive можно не указывать
};Квадратные скобки вокруг имени свойства ([isActive]) обозначают, что свойство необязательное.
Типы-функции
С помощью @typedef можно описывать не только объекты, но и функции. В проекте Kelp это используется для описания колбэков:
/**
* @typedef {function(string): void} NotificationCallback
* Функция, принимающая строку и не возвращающая значения
*/
/** @type {NotificationCallback} */
const showAlert = (message) => {
alert(message);
};Пример из проекта Kelp: типизация веб-компонента
Крис использует @typedef для документирования публичного API своих веб-компонентов. Рассмотрим пример с компонентом <kelp-toast>:
/**
* @typedef {Object} Toast
* @property {(message: string, type?: 'success'|'error'|'info') => void} notify
* @property {() => void} hide
* @property {boolean} visible
*/Теперь при получении ссылки на компонент можно указать этот тип:
/** @type {Toast | null} */
const toastEl = document.querySelector('kelp-toast');
if (toastEl) {
// Редактор знает о существовании метода notify и его параметрах
toastEl.notify('Операция выполнена успешно', 'success');
}Импорт типов из других файлов
По мере роста проекта типы удобно выносить в отдельные файлы. Для импорта типов используется специальный синтаксис:
// types.js
/**
* @typedef {Object} Config
* @property {string} apiUrl
* @property {number} timeout
*/
// main.js
/**
* @typedef {import('./types.js').Config} Config
*/
/** @param {Config} config */
function initialize(config) {
// ...
}Этот подход удобен для типов, которые используются в разных модулях проекта.
Расширение существующих типов
Иногда требуется добавить свойства к типам, определённым в сторонних библиотеках или в DOM. Для этого используется объединение типов (intersection):
/**
* @typedef {HTMLInputElement & { validator: (value: string) => boolean }} ValidatedInput
*/
/** @type {ValidatedInput} */
const input = document.createElement('input');
input.validator = (value) => value.length > 3;Преимущества использования @typedef
- Единообразие — один раз определив тип, вы гарантируете, что везде используются одинаковые структуры
- Документирование —
@typedefслужит живой документацией - Удобство рефакторинга — при изменении структуры достаточно поправить определение типа, и все места использования подсветятся ошибками
- Переиспользование — типы можно импортировать в любые файлы проекта
В проекте Kelp автор вынес общие типы в отдельный файл types.d.ts, который подключается в jsconfig.json. Это позволяет использовать их во всём проекте без многократного переопределения.
Дальнейшие шаги: от быстрого старта к полноценному внедрению
Рассмотренные приёмы покрывают большинство повседневных задач при работе с JSDoc. Однако в проектах, развивающихся длительное время, возникают дополнительные потребности: автоматизация проверок, генерация документации, интеграция с TypeScript-экосистемой. Рассмотрим направления, в которых можно развивать описанный подход.
Автоматическая проверка в CI/CD
Ручная проверка типов в редакторе полезна, но не гарантирует, что код с ошибками попадёт в основную ветку. Для этого проверку типов включают в процесс непрерывной интеграции.
В проекте Kelp для этого используется команда tsc --noEmit, которую мы добавили на этапе настройки. В CI-системе (GitHub Actions, GitLab CI и т.п.) достаточно выполнить:
npm install
npm run type-checkЕсли в коде есть ошибки типов, команда завершится с ненулевым кодом, и CI остановит процесс сборки или слияния.
Пример конфигурации для GitHub Actions:
name: Type Check
on: [push, pull_request]
jobs:
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npm run type-checkГенерация файлов деклараций (.d.ts)
Если вы разрабатываете библиотеку, пользователи могут использовать её в проектах на TypeScript. Для них важна наличие файлов деклараций, описывающих типы.
TypeScript умеет генерировать .d.ts файлы из JSDoc-комментариев. Для этого создаётся отдельная конфигурация:
// tsconfig.types.json
{
"include": ["src/**/*.js"],
"compilerOptions": {
"allowJs": true,
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "dist/types"
}
}И соответствующий скрипт в package.json:
{
"scripts": {
"build:types": "tsc -p tsconfig.types.json"
}
}После выполнения в папке dist/types появятся .d.ts файлы, которые можно распространять вместе с библиотекой.
Постепенное ужесточение проверок
В начале проекта или при добавлении JSDoc в существующий код удобно использовать мягкие настройки проверки. По мере добавления типов строгость можно повышать.
Ключевые опции jsconfig.json для поэтапного внедрения:
| Опция | Назначение |
|---|---|
strict: false | Начальный этап, базовые проверки |
noImplicitAny: true | Запрещает неявный any |
strictNullChecks: true | Включает строгую проверку null и undefined |
strict: true | Максимальная строгость |
В проекте Kelp используется близкий к максимальному уровень строгости, что позволяет выявлять максимум потенциальных ошибок.
Интеграция с существующими TypeScript-проектами
JSDoc-типизация не требует изоляции — проект на JavaScript с JSDoc может взаимодействовать с настоящими TypeScript-модулями. TypeScript Language Server понимает оба подхода и обеспечивает корректную проверку на границах модулей.
Это позволяет, например, постепенно мигрировать части проекта на TypeScript, не переписывая всё сразу, или использовать TypeScript-библиотеки в JavaScript-коде с JSDoc.
Полное руководство по продвинутой настройке
Рассмотренные возможности — лишь верхушка айсберга. Более детально темы настройки компилятора, генерации деклараций, интеграции с CI/CD и решения сложных задач типизации раскрыты в отдельном материале:
Полное руководство по настройке TypeScript Compiler для JSDoc
В нём разобраны:
- Все опции
tsconfig.jsonдля работы с JSDoc - Генерация карт деклараций (declarationMap)
- Работа с глобальными типами
- Типизация сложных сценариев (перегрузки функций, дженерики)
- Интеграция с Webpack, Rollup и другими сборщиками
- Отладка проблем с типами
Заключение
Мы рассмотрели практическую реализацию подхода, который Крис Фердинанди применил в своём проекте Kelp: использование JSDoc в связке с TypeScript Compiler для получения преимуществ статической типизации без перехода на TypeScript как язык.
Основные выводы
Подход работоспособен в реальных проектах
Проект Kelp — не учебный пример, а работающий UI-кит. Тот факт, что автор выбрал именно JSDoc, подтверждает: этот подход пригоден не только для небольших скриптов, но и для поддерживаемых библиотек.
Порог входа минимален
Для начала достаточно установить TypeScript как dev-зависимость, создать jsconfig.json с несколькими строчками и начать добавлять JSDoc-комментарии в ключевые места кода. Остальной код может оставаться без изменений.
Результат ощутим сразу
Уже после базовой настройки редактор начинает:
- Подсвечивать несоответствия типов
- Предлагать автодополнение с учётом контекста
- Показывать документацию при наведении
Есть путь для развития От быстрого старта можно двигаться к более продвинутым практикам:
- Автоматическая проверка в CI
- Генерация файлов деклараций для библиотек
- Постепенное ужесточение правил проверки
Когда этот подход выбирать
JSDoc-типизация хорошо подходит для:
- Существующих JavaScript-проектов — можно добавить типизацию постепенно, без рефакторинга
- Библиотек и инструментов — пользователи получат и работающий код, и подсказки в редакторе
- Прототипов и скриптов — типизация помогает не допустить ошибок, но не замедляет разработку
- Команд, не готовых к TypeScript — можно получить часть преимуществ без изучения нового синтаксиса
Когда лучше рассмотреть полноценный TypeScript
Более сложные сценарии могут потребовать перехода на .ts файлы:
- Необходимость в продвинутых возможностях системы типов (условные типы, сопоставленные типы и т.п.)
- Работа с большим количеством сторонних TypeScript-библиотек
- Команда уже использует TypeScript и хочет единообразия
- Требуется строгий контроль типов на всём пути сборки
Следующие шаги
Если вы хотите углубиться в тему и настроить профессиональный процесс работы с типами на JavaScript, рекомендуем обратиться к полному руководству по настройке TypeScript Compiler для JSDoc. В нём детально разобраны все аспекты конфигурации, интеграции с инструментами сборки и решения сложных задач типизации.