Все статьи | Код

Как проверять типы данных в JavaScript с помощью JSDoc: подробное руководство

Как проверять типы данных в JavaScript с помощью JSDoc: подробное руководство главное изображение

Это перевод статьи JavaScript Type Linting, написанной Робертом Биггсом. Повествование ведётся от имени автора оригинала.

Существует распространённое заблуждение: якобы единственный способ избежать ошибок типизации в JavaScript — писать код на языке со статической типизацией, который компилируется в JavaScript. С этой целью используют ClojureScript, Elm, ReasonML, TypeScript и так далее. В настоящее время самое популярное решение — TypeScript.

На самом деле существует альтернативный способ борьбы с ошибками типизации — проверка или линтинг типов. Он реализуется с помощью Visual Studio Code и сервера TypeScript, который работает в фоновом режиме. Когда вы пишете на JavaScript, TypeScript анализирует типы данных и сообщает об ошибках. Это происходит в режиме реального времени, поэтому нет нужды выполнять сборку — типы проверяются автоматически, когда вы вводите код. Что ещё важнее, вы пишете на JavaScript, а не на TypeScript. Есть три способа проверки типов в Visual Studio Code:

  1. На уровне файла.
  2. Глобально для всех проектов.
  3. Только для конкретного проекта.

Как настроить проверку типов

Как отмечалось выше, это можно сделать на уровне файла, глобально для всех проектов и для отдельного проекта. Рассмотрим эти способы подробнее.

Проверка типов на уровне файла

Если вы работаете в Visual Studio Code, линтинг типов можно включить несколькими способами. Самый простой — добавить в начале файла такой комментарий:

// @ts-check

Проверка типов для всех проектов

Если не хотите каждый раз добавлять комментарии в файлы, включите проверку в редакторе. Найдите в настройках пункт Check JS и отметьте его галочкой.

Настройка VS Code

Так вы включаете линтинг типов для любого файла с кодом JavaScript, который редактируете.

Проверка типов для конкретного проекта

Если не хотите включать проверку типов для всех проектов по умолчанию, ограничьтесь конкретным проектом. Соответствующие настройки надо указать в файле settings.json. Сначала создайте директорию .vscode в корне проекта. В этой директории создайте файл settings.json, в котором включите проверку типов:

{
  "javascript.implicitProjectConfig.checkJs": true
}

Третий способ предпочтительный, так как с его помощью можно включить линтинг типов во всех файлах JavaScript в выбранном проекте. Чтобы отключить проверку типов в каком-либо файле, например, в файлах, созданных Gulp или другими сборщиками, просто добавьте в начале файла комментарий:

// @ts-nocheck

Вывод типов

Если вы включили проверку типов одним из указанных выше способов, Visual Studio Code с помощью сервера TypeScript парсит файлы JavaScript и определяет используемые типы данных. По умолчанию это происходит с помощью вывода типов (Type Inference). Это значит, что если вы объявляете переменную или константу для строки или числа, для этой переменной будет определён соответствующий тип данных.

// Для этой константы будет определён тип данных string:
const name = 'Joe';
// Для этой константы будет определён тип данных number:
const age = 100;

Многие языки программирования со строгой типизацией используют вывод типов в качестве быстрого способа определения типов. TypeScript тоже поддерживает такой подход. Но это не лучший способ, так как если TypeScript точно не определяет тип данных, он использует any. Это плохо, так как если типы сводятся к any, это делает бессмысленной их проверку.

Указание типов в комментариях JSDoc

Вы можете указывать типы данных для TypeScript в коде JavaScript с помощью комментариев JSDoc. Они представляют собой валидные комментарии JavaScript, не влияют на выполнение кода и не требуют компиляции. JavaScript с комментариями JSDoc можно запускать в браузере или на Node.js.

Этот подход обеспечивает такие же возможности, как TypeScript — удобные автодополнения и уведомления о некорректном использовании типов. Если вы передадите в функцию некорректный аргумент, Visual Studio Code сразу уведомит вас. Так же происходит при использовании TypeScript. Благодаря этому можно не использовать файлы d.ts, так как комментарии с указанием типов можно писать в JavaScript.

JSDoc предназначен для документирования JavaScript. Кажется, этот инструмент работает лучше подхода с использованием TypeScript и аннотаций типов, интерфейсов и других структур, которых нет в JavaScript. В TypeScript реализованы паттерны, характерные для C# и Java, поэтому он не похож на JavaScript. Скорее, он похож на C# и Java. Если вам нравятся эти языки, вероятно, вам понравится и сам TypeScript. В свою очередь, JSDoc не влияет на то, как выглядит JavaScript. Вы просто пишете обычный код JavaScript с комментариями, в которых определяются типы, использованные в коде.

TypeScript — язык, который компилируется в JavaScript. Он обеспечивает безопасность типов до сборки кода. После сборки у вас остаётся чистый JavaScript. Всё, что делает TypeScript, оказывается бесполезным, как только вы запускаете код JavaScript в браузере или Node.js. Поэтому единственной гарантией корректного использования типов при выполнении кода JavaScript остаются защитников типов (type guards). Если переживаете, что ошибки типизации сломают код, можете использовать type guards. Статическая типизация не гарантирует отсутствия ошибок при выполнении кода.

С другой стороны, информация о типах в JSDoc — это валидные комментарии JavaScript. Если хотите, можете оставлять их в коде — они не влияют на его выполнение. Но в целом надо стремиться минимизировать код, а это предполагает отказ от комментариев.

Также полезно Что такое __dirname в JavaScript.

Примеры типобезопасного JavaScript

Чтобы показать, как выглядит типобезопасный JavaScript с комментариями JSDoc, я использовал npm-пакет @composi/core. Он написан с использованием ECMAScript 2015. В нём не используется файл d.ts, а информация о типах передаётся через комментарии JSDoc. Если импортировать этот модуль в проект, вся информация о типах становится доступной конечному пользователю.

На иллюстрации ниже можно увидеть функции, импортированные из @composi/core. Если навести курсор на функцию h, появляется всплывающая подсказка с ожидаемыми аргументами и их типами.

проверка типов — пример

На следующей иллюстрации видно, что происходит при наведении курсора на функцию render. На всплывающей подсказке появляются ожидаемые параметры и их типы. Обратите внимание, что VNode и container — обязательные параметры, а hydrateThis — опциональный параметр.

пример работы JSDoc

На следующей иллюстрации видно, что происходит, если передать в функцию render некорректные аргументы. Всплывающая подсказка информирует, что мы не передали обязательный аргумент container. Также мы видим два варианта быстрого исправления ошибки. Заметьте, что в верхней части всплывающей подсказки есть явное сообщение о том, что функция ожидает два или три аргумента, а мы передали в неё только один аргумент.

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

TypeScript работает в фоне, проверка типов происходит в реальном времени

На следующей иллюстрации показано, что происходит после нажатия Peek Problem.

пример проверки типов

Если нажать на название файла, Visual Studio Code перебрасывает нас в исходный код импортированного модуля и показывает параметры функции render.

скриншот VS Code с линтингом типов

Далее мы передаём в функцию второй аргумент. Предупреждение меняется — появляется сообщение, что число 123 — это невалидный аргумент, так как функция render ожидает первым аргумент с типом VNode.

иллюстрируем работу TS и JSDoc

На следующей иллюстрации видно, что мы исправляем ошибку и передаём в функцию подходящий аргумент. Теперь появляются проблемы со вторым аргументом — функция ожидает другой тип данных.

типы аргументов функций

Как видно из примеров выше, можно пользоваться типобезопасным JavaScript без необходимости компилировать в JavaScript код, написанный на языке с поддержкой статической типизации. Это происходит благодаря проверке типов, которую обеспечивают Visual Studio Code и TypeScript. Да, мы используем TypeScript, но он работает в фоне и выступает в качестве линтера типов. Мы всё время пишем на чистом JavaScript, а не на TypeScript. Но TypeScript понимает типы данных, которые мы используем в JavaScript, благодаря комментариям JSDoc.

Я верю, что использование комментариев JSDoc — очень важная находка для экосистемы JavaScript, так как она обеспечивает типобезопасность без необходимости писать код на TypeScript или другом языке со статической типизацией с последующей компиляцией в JavaScript.

Как определять типы JavaScript

С помощью JSDoc можно документировать типы данных, которые используются в вашем JavaScript-коде. В отличие от других решений, которые добавляют искусственную систему типов в JavaScript, JSDoc работает с типами данных самого JavaScript. В JavaScript слабая типизация, а типы присваиваются данным в момент выполнения кода. Суть динамической типизации в JavaScript можно объяснить термином «утиная типизация» — если что-то ходит как утка и крякает как утка, это и есть утка. Это значит, что если два абсолютно разных объекта имеют одинаковые свойства, их можно использовать как взаимозаменяемые, если вы используете только общие свойства. В JavaScript это часто реализуется с помощью доступа к свойству родительского объекта через его цепочку прототипов. Полный список типов, которые можно определить с помощью JSDoc, указан в документации. Ниже идёт краткое описание.

Примитивные типы vs. объекты

В JavaScript есть следующие примитивные типы данных:

  • undefined
  • null
  • boolean
  • число
  • строка

Примитивные типы в JavaScript неизменяемые. Они сравниваются по значению. Все сложные типы — объекты. Они изменяемые и сравниваются по ссылке. У примитивных типов строка и число есть конструкторы — глобальные объекты String и Number. Другие структуры данных, включая массивы, объекты, функции и классы — относятся к типу данных объект.

Базовые типы

JSDoc — соглашение об использовании стандартных комментариев JavaScript. Обычно комментарии используются в следующем формате:

/**
 * Ваш комментарий...
 */

С помощью специальных терминов и форматов внутри этих комментариев определяются типы JavaScript.

@type{}

Самый важный тег JSDoc — @type, за которым следуют фигурные скобки {}. Когда вы определяете тип, он всегда заключается в фигурные скобки.

/** 
 * Для примера возьмём простую строку.
 * @type {string} name
 */
const name = 'Joe';

Аналогично можно указать тип для переменной sum в примере ниже.

/** 
 * Простой пример использования типа данных число.
 * @type {number} sum
 */
let sum = 2 + 2;

Типы данных в комментариях JSDoc можно писать с прописной или строчной буквы. Согласно текущему соглашению, предпочтительнее писать названия типов со строчной буквы.

Тип any

Как отмечалось выше, когда TypeScript не может определить тип данных, он присваивает им тип any. Он не входит в число валидных типов данных JavaScript. Так TypeScript помечает данные, тип которых не надо проверять. Вы также можете вручную пометить данные типом any. Зачем это нужно? Вероятно, это полезно в редких случаях, когда вы не уверены, какой тип данных должен использоваться. Это связано со случайным приведением типов, которое может выполнить JavaScript. Тип any можно обозначить несколькими способами.

/**
 * Следующие записи эквивалентны:
 * @type {any}
 * @type {*}
 * @type {?}
 */

Обратите внимание, есть неявный способ определить тип any с помощью ключевого слова Object:

/**
 * Такие записи определяют тип any:
 * @type {Object}
 * @type {object}
 */

Разработчики TypeScript решили присваивать тип any при использовании ключевого слова Object после анализа большого количества репозиториев, в которых использовался JSDoc. Многие программисты используют термин «object» неаккуратно, что не позволяет достоверно определить тип данных, который имеется в виду. Поэтому когда TypeScript видит в комментариях JSDoc соответствующее ключевое слово, он присваивает данным тип any.

Дальше рассказывается, как правильно определять тип данных object.

Сложные типы

Сложные типы данных порождают некоторые сложности. Как вы только что узнали, при использовании ключевых слов Object или object TypeScript определяет тип данных any. Чтобы определить тип объекта, нужно явно указать его. Если вы имеете дело с пустым объектным литералом, тип можно указать так:

/**
 * Указываем тип пустого объектного литерала.
 * @type {{}} obj
 */
const obj = {};

Такой подход уместен, если вы не планируете добавлять свойства в объект. Если вы когда-то попробуете сделать это, то столкнётесь с проблемой, так как ранее определили тип — пустой объектный литерал.

/**
 * Указываем тип — пустой объектный литерал.
 * @type {{}} obj
 */
const obj = {}
obj.name = 'Jane' // Ошибка, у объекта нет свойства name.

Проблему можно решить, если при определении типа сообщить TypeScript о свойствах, которые могут появиться в объекте.

/**
 * Определяем объектный литерал с двумя свойствами:
 * @type {{name: string, age: number}} obj1
 */
const obj1 = {} // Ошибка, отсутствуют свойства name и age.
/**
 * Определяем объектный литерал с двумя свойствами:
 * @type {{name: string, age: number}} obj2
 */
const obj2 = {name: 'Sam', age: 32} // Здесь нет ошибок.

Открытые объекты

Проблем со свойствами можно избежать, если указывать при определении типа открытые объекты.

**
 * Определяем открытый объект.
 * Указываем, что по умолчанию его свойства будут строками:
 * @type {Object<string, any>} obj
 */

Если планируете, что значениями свойств будут числа, это можно указать так:

/**
 * Определяем открытый объект.
 * Указываем, что по умолчанию значениями свойств будут числа:
 * @type {Object<string, number>} obj
 */

Определение открытого объекта позволяет без проблем добавлять в него свойства.

/**
 * Определяем открытый объект.
 * @type {Object} obj
 */
obj.name = 'Same' // Добавляем свойство в объект.

Открытые объекты решают проблемы, которые есть у пустых объектных литералов. Но в данном случае у нас нет полезной информации о свойствах объектов. Всем новым свойствам присваивается тип any. Чтобы получить строгую информацию о типах свойств, необходимо создавать пользовательские свойства. Об этом пойдёт речь ниже.

На Хекслете есть раздел «Треки», в котором собраны курсы для глубокой проработки отдельных тем, например, асинхронности в JavaScript, ООП, тестирования. Регистрируйтесь и начинайте учиться, треки полезны как для начинающих, так и для опытных программистов!

Массивы

Мы часто работаем с данными, упакованными в массивы. JSDoc позволяет легко определять типы массивов, с которыми мы работаем. Самый простой способ определить тип массива — определение типа данных, которые содержатся в этом массиве:

/**
 * Массив данных неопределённого типа:
 * @type {any[]} arr1 // Можно использовать такую запись {*[]}
 */

Также можно определить массивы строк, чисел или объектов:

/**
 * Массив строк:
 * @type {string[]} arr2
 */
/**
 * Массив чисел:
 * @type {number[]} arr3
 */

Можно определить массив объектов, это делается так:

/**
 * Массив объектов:
 * @type {Object<string, any>[]} arr4
 */

Когда мы рассмотрим создание пользовательских типов, вы сможете определять массивы пользовательских типов.

Функции

Проще всего определять функции с помощью тега @function.

/**
 * Определяем функцию.
 * @function
 */
function doIt() {};

Параметры функции

У функций могут быть параметры. Их можно определять с помощью тега @param. Далее указывается тип параметра в фигурных скобках, за которыми следует имя параметра.

/**
 * Определяем функцию, которая принимает два аргумента: name и age.
 * @param {string} name
 * @param {number} age
 * returns {{name: string, age: number} Person object
 */
 const makePerson = (name, age) => {
   return {name, age};
 };

Колбэки

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

Классы

Поскольку работать с классами непросто, лучше обратиться к документации.

Пользовательские типы

Иногда нужно использовать пользовательские типы, чтобы определить, что делает ваш код. В TypeScript для определения пользовательских типов применяются интерфейсы, так же как в Java или C#. В JavaScript нет интерфейсов, но вы можете получить нужный результат с помощью тега @typedef. Используйте его, чтобы определить основу пользовательского типа, а для определения его свойств используйте теги @prop или @property. Рассмотрим это на примере создания пользовательского типа Person.

/**
 * @typedef {Object} Person
 * @property {string} name
 * @property {number} age
 */

Мы определили тип Person, который можно использовать так:

/** 
 * Анализируем объект Person.
 * @param {Person} person
 */
const analysePerson = (person) => { 
  alert(`The person's name is ${person.name} and the age is ${person.age}`);
};

Мы определили, что аргументом функции выступает объект Person. Поэтому можно наверняка рассчитывать, что в этом объекте есть свойства name и age.

Импорт типов

Если вы уже определили тип в одном модуле, можете импортировать его в другой модуль и использовать в нём. В импортах нужно указывать относительные пути. Синтаксис импорта типа выглядит так:

import('path').Type

Вместо path надо указать путь к файлу, в котором определён тип. После закрывающей скобки ставится точка и указывается тип, который вы импортируете. После этого TypeScript сможет использовать импортированный тип в соответствующем модуле.

/**
 * @typedef {import('../types').Vehicle} Car
 */

Необязательные типы

Иногда нужно определять необязательные параметры функций или свойства объектов. Опциональность можно отметить с помощью квадратных скобок. В примере ниже свойство age необязательное.

/**
 * @typedef {Object<string, any>} Obj
 * @property {string} name
 * @property {number} [age]
 */

Таким же способом можно указывать необязательные параметры функций. В примере ниже в функцию можно не передавать аргумент age, ошибки не будет.

/** 
 * @param {string} name
 * @param {number} [age]
 */
const makePerson = (name, age) => { 
  return {name, age};
};

Union Types (объединение типов)

Иногда приходится работать со значениями разных типов. Это становится проблемой, если речь идёт о параметрах функций или методов. В этом случае можно использовать Union Types. Для этого надо перечислить возможные типы и разделить их пайпами:

/**
 * @param {string | number} age
 */
const announceAge = (age) => {
  alert(`The person's age is ${age}`);
};

Такая запись позволяет передавать строки и числа в функцию announceAge.

Приведение типов

В вашем коде происходит приведение типов. Это возможно, когда в коде встречается значение с неоднозначным типом, а TypeScript может определить методы, которые вы вызываете с этим значением. Для приведения типов нужно определить тип и указать свойство в скобках. В примере ниже есть переменная sum, значение которой может быть разных типов. Попытка вызвать метод toFixed с этой переменной может привести к ошибке. Чтобы этого не произошло, нужно привести её тип к числу:

/** @type{number} */ (sum).toFixed(2)

Пропуск строки

Если у вас трудности с корректным определением типов, можно отключить проверку типов для конкретной строки. Это делается так:

// @ts-ignore

Уменьшаем объём комментариев JSDoc

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

На иллюстрации показан способ определения сложных типов с использованием более лаконичного варианта комментариев JSDoc.

уменьшаем объём комментариев

На следующей иллюстрации показано определение этих типов на TypeScript.

объём кода TypeScript

Как видно, разница в объёме кода невелика. Сокращённая версия JSDoc теряет подсветку синтаксиса, но всё равно остаётся читабельной. Поэтому выбор между использованием TypeScript и JSDoc — вопрос предпочтений. Хотите писать на чистом JavaScript и иметь типобезопасность? Пользуйтесь JSDoc. Хотите писать на другом языке, который обеспечивает типобезопасность и компилируется в JavaScript? Тогда используйте TypeScript, Reason, Elm и так далее.

Как настроить проект

Для вашего удобства я создал на GitHub проект check-js, в котором есть линтинг типов одновременно в режиме реального времени и при сборке. Откровенно говоря, это очень простой проект. В нём используются ESLint, Prettier, юнит-тесты с помощью Jest и проверка типов. Вы можете самостоятельно настроить ESLint и Prettier, а также использовать другой инструмент для юнит-тестирования на своё усмотрение. Проект доступен по ссылке.

tsconfig.json

Чтобы Visual Studio Code лучше понимал структуру вашего проекта и типы, которые в нём используются, можно создать в корне проекта файл tsconfig.json и указать в нём соответствующие данные. Вот пример файла tsconfig.json.

{
  "compilerOptions": {
    "target": "es6",
    "allowJs": true,
    "checkJs": true,
    "moduleResolution": "node",
    "alwaysStrict": true,
    "strictNullChecks": false,
    "emitDeclarationOnly": true,
    "declaration": true,
    "outDir": "types",
    "removeComments": false
  },
  "files": [
    "src/index.js"
  ],
  "exclude": [
    "__tests__",
    "node_modules",
    "types"
  ]
}

Обратите внимание, как указывается путь: src/index.js. Также обратите внимание на свойство exclude, в котором указаны исключения. Однозначно нужно указывать здесь node_modules. Также TypeScript должен игнорировать директорию types, в которой находятся автоматически сгенерированные файлы d.ts. Директория с тестами также указана в исключениях, в данном примере она называется __tests__. Если вы используете конфигурацию, указанную в примере выше, проверку типов в терминале можно запускать с помощью следующего скрипта:

"scripts": { 
  "checkjs: "tsc"
}

Кроме проверки типов, он также создаёт файлы с определениями типов d.ts. Вы можете обновить файл package.json, чтобы сделать эти типы доступными вашим пользователям:

typings: "types"

Конечно, сначала нужно установить TypeScript не ниже версии 3.7.3. Для этого воспользуйтесь командой:

npm i -D typescript@latest

После этого можно запустить проверку:

npm run checkjs

Пакетный импорт

Если вы используете TypeScript версии 3.7.3 и выше, можно пользоваться пакетным импортом типов. Мне нравится такой подход: указываем все типы в файле types.js в корневой директории, а когда необходимо импортировать типы в другой модуль, делаем это так:

import * as MyTypes from '../types'

Используйте подходящее пространство имён для своего проекта. Из этого пространства имён можно получить доступ к типам:

/**
 * @typedef {MyTypes.Person} Person
 * @typedef {MyTypes.User} User 
 * @typedef {MyTypes.Business} Business
 */

Примеры

Если хотите посмотреть, как работает проверка типов в реальности, загляните в проекты из этого репозитория. В них реализован линтинг типов в реальном времени, а также возможность запустить проверку в терминале с помощью команды npm test.

Адаптированный перевод статьи JavaScript Type Linting by Robert Biggs. Мнение администрации Хекслета может не совпадать с мнением автора оригинальной публикации.

Аватар пользователя Дмитрий Дементий
Дмитрий Дементий 18 сентября 2020
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Фронтенд-разработчик
Профессия
Разработка фронтенд-компонентов веб-приложений
1 июня 10 месяцев
Иконка программы Python-разработчик
Профессия
Разработка веб-приложений на Django
1 июня 10 месяцев
Иконка программы PHP-разработчик
Профессия
Разработка веб-приложений на Laravel
1 июня 10 месяцев
Иконка программы Node.js-разработчик
Профессия
Разработка бэкенд-компонентов веб-приложений
1 июня 10 месяцев
Иконка программы Fullstack-разработчик
Профессия
Новый
Разработка фронтенд и бэкенд компонентов веб-приложений
1 июня 16 месяцев
Иконка программы Верстальщик
Профессия
Вёрстка с использованием последних стандартов CSS
в любое время 5 месяцев
Иконка программы Java-разработчик
Профессия
Разработка приложений на языке Java
1 июня 10 месяцев
Иконка программы Разработчик на Ruby on Rails
Профессия
Создает веб-приложения со скоростью света
1 июня 5 месяцев