JSDoc на практике: как добавить типизацию в JS-проект без TypeScript

Переходим от теории к практике. Разберём реальный кейс: как разработчик Крис Фердинанди добавил проверку типов в свой UI-кит Kelp, используя JSDoc и TypeScript Compiler, но без единого .ts файла. Рассмотрим пошаговую настройку окружения, основные приёмы типизации переменных, функций и DOM-элементов, создание собственных типов с @typedef, а также следующие шаги — от автоматической проверки в CI до генерации .d.ts файлов. Все примеры адаптированы для самостоятельного повторения.

Введение

В предыдущей статье мы обосновали, почему JSDoc является прагматичной альтернативой TypeScript. Были рассмотрены преимущества подхода, его ограничения и сценарии, в которых отказ от транспиляции даёт ощутимые выгоды.

Теперь перейдём к практической реализации. В качестве примера возьмём реальный проект — UI-кит Kelp, разработанный Крисом Фердинанди. Автор столкнулся с типичной дилеммой: с одной стороны, статическая типизация полезна для поддержки кода, с другой — не хотелось отказываться от чистого JavaScript и усложнять процесс сборки.

Решение было найдено в связке JSDoc и TypeScript Compiler. Рассмотрим, как именно автор настроил проверку типов, какие приёмы использовал и с какими сложностями столкнулся. Все примеры адаптированы для самостоятельного повторения.

Необходимые компоненты

Прежде чем перейти к примерам, уточним, что именно потребуется для реализации подхода, использованного в проекте Kelp.

Компилятор TypeScript

Основной инструмент — компилятор TypeScript (tsc). Это может звучать противоречиво, поскольку мы договорились не использовать TypeScript как язык. Однако компилятор выполняет здесь служебную роль: он отвечает за проверку типов, но не участвует в транспиляции кода.

Установка выполняется стандартным образом:

npm install -D typescript

После установки становятся доступны:

Конфигурационный файл

Для работы с JavaScript-файлами требуется файл конфигурации — jsconfig.json или tsconfig.json с определёнными настройками. Его назначение — указать компилятору, какие файлы проверять и насколько строго это делать.

В проекте Kelp используется именно такой подход: TypeScript установлен как зависимость, создан минимальный конфигурационный файл, а весь код остаётся в .js файлах с JSDoc-аннотациями.

Среда разработки

Для полноценной работы потребуется редактор с поддержкой TypeScript Language Server:

Все примеры, которые будут рассматриваться далее, работают в любом из перечисленных редакторов при условии корректной настройки.

Что не потребуется

Важно подчеркнуть, что нам не нужны:

Весь код остаётся ванильным 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 — необходимо установить пакеты:

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;
}

Ошибка возникает потому, что:

  1. event может быть любым типом события, не обязательно ChangeEvent
  2. event.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

  1. Единообразие — один раз определив тип, вы гарантируете, что везде используются одинаковые структуры
  2. Документирование@typedef служит живой документацией
  3. Удобство рефакторинга — при изменении структуры достаточно поправить определение типа, и все места использования подсветятся ошибками
  4. Переиспользование — типы можно импортировать в любые файлы проекта

В проекте 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

В нём разобраны:

Заключение

Мы рассмотрели практическую реализацию подхода, который Крис Фердинанди применил в своём проекте Kelp: использование JSDoc в связке с TypeScript Compiler для получения преимуществ статической типизации без перехода на TypeScript как язык.

Основные выводы

  1. Подход работоспособен в реальных проектах

    Проект Kelp — не учебный пример, а работающий UI-кит. Тот факт, что автор выбрал именно JSDoc, подтверждает: этот подход пригоден не только для небольших скриптов, но и для поддерживаемых библиотек.

  2. Порог входа минимален

    Для начала достаточно установить TypeScript как dev-зависимость, создать jsconfig.json с несколькими строчками и начать добавлять JSDoc-комментарии в ключевые места кода. Остальной код может оставаться без изменений.

  3. Результат ощутим сразу

    Уже после базовой настройки редактор начинает:

    • Подсвечивать несоответствия типов
    • Предлагать автодополнение с учётом контекста
    • Показывать документацию при наведении
  4. Есть путь для развития От быстрого старта можно двигаться к более продвинутым практикам:

    • Автоматическая проверка в CI
    • Генерация файлов деклараций для библиотек
    • Постепенное ужесточение правил проверки

Когда этот подход выбирать

JSDoc-типизация хорошо подходит для:

Когда лучше рассмотреть полноценный TypeScript

Более сложные сценарии могут потребовать перехода на .ts файлы:

Следующие шаги

Если вы хотите углубиться в тему и настроить профессиональный процесс работы с типами на JavaScript, рекомендуем обратиться к полному руководству по настройке TypeScript Compiler для JSDoc. В нём детально разобраны все аспекты конфигурации, интеграции с инструментами сборки и решения сложных задач типизации.

Комментарии


Дополнительные материалы

Предыдущая Статья

Как писать CSS с @scope