TypeScript в проектах на JavaScript (JSDoc)
Комментарии JSDoc позволяют добавить проверку типов TypeScript в обычные файлы JavaScript, не переименовывая их в .ts.
Это идеально подходит, когда вы хотите переносить проект шаг за шагом или когда вам просто нужна безопасность типов в кодовой базе на JavaScript.
Примечание: примеры на этой странице написаны как JavaScript с типами JSDoc. Сохраните их как файлы .js с // @ts-check в начале (или с включённым checkJs), чтобы увидеть описанную проверку типов. В файле .ts компилятор игнорирует типы JSDoc и опирается на обычные аннотации.
Начало работы
Чтобы TypeScript проверял ваши файлы JavaScript, нужно:
- Создать файл
tsconfig.json(если у вас его ещё нет) - Включить в нём
checkJsили поставить// @ts-checkв начале тех файлов, которые вы хотите проверять
Пример: JSDoc для безопасности типов
Комментарий JSDoc над функцией сообщает TypeScript, что оба параметра и возвращаемое значение являются числами.
// @ts-check в начале файла.
Напишите функцию add(a, b), задокументированную с помощью @param {number} a, @param {number} b и @returns {number}, которая возвращает сумму. Нужно только объявление — программа ничего не выводит.
Вывод: программа на TypeScript и захваченный вывод
// @ts-check
/**
* Складывает два числа.
* @param {number} a
* @param {number} b
* @returns {number}
*/
function add(a, b) {
return a + b;
}
Объекты и интерфейсы
Вы можете описать форму объекта прямо в теге JSDoc или дать ей имя с помощью @typedef, чтобы тип можно было переиспользовать во многих местах.
Встроенные типы объектов
Тип параметра записан прямо на месте как тип объектного литерала. В проверяемом файле .js вызов greet без lastName отмечается как ошибка.
// @ts-check задокументируйте greet(person) с помощью @param {{ firstName: string, lastName: string, age?: number }} person и верните Привет, <firstName> <lastName>. Вызовите её один раз с полным объектом (John Doe) и один раз только с firstName: "Jane", пометив второй вызов комментарием об ошибке отсутствующего lastName, о которой сообщает проверяемый файл .js. Ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод
// @ts-check
/**
* @param {{ firstName: string, lastName: string, age?: number }} person
*/
function greet(person) {
return `Привет, ${person.firstName} ${person.lastName}`;
}
greet({ firstName: 'John', lastName: 'Doe' }); // OK
greet({ firstName: 'Jane' }); // Ошибка: свойство 'lastName' отсутствует
Использование @typedef для сложных типов
@typedef вместе со строками @property определяет именованный тип User. Квадратные скобки помечают необязательное свойство, а объединение в круглых скобках ограничивает role несколькими значениями.
@typedef и @property, описывающие тип объекта в JSDoc.
Определите typedef User с id: number, username: string, необязательным email, полем role, ограниченным значениями "admin" | "user" | "guest", и методом getFullName, возвращающим строку. Объявите currentUser с /** @type {User} */ (id 1, username johndoe, role admin, getFullName() возвращает John Doe) и выведите currentUser.role.
Вывод: программа на TypeScript и захваченный вывод
// @ts-check
/**
* @typedef {Object} User
* @property {number} id - Идентификатор пользователя
* @property {string} username - Имя пользователя
* @property {string} [email] - Необязательный адрес электронной почты
* @property {('admin'|'user'|'guest')} role - Роль пользователя
* @property {() => string} getFullName - Метод, возвращающий полное имя
*/
/** @type {User} */
const currentUser = {
id: 1,
username: 'johndoe',
role: 'admin',
getFullName() {
return 'John Doe';
}
};
// TypeScript будет подсказывать свойства типа User
console.log(currentUser.role);
Расширение типов
Пересечение (Point & { z: number }) строит новый тип на основе существующего. В проверяемом файле .js объект без z — это ошибка, которую и ожидает комментарий @ts-expect-error. При компиляции как файла .ts типы JSDoc игнорируются, поэтому компилятор вместо этого сообщает, что директива не использована.
Point как { x: number, y: number }, а Point3D как Point & { z: number }. Объявите point3d с типом Point3D и значениями x 1, y 2, z 3, затем объект point2d без z, перед которым стоит комментарий // @ts-expect-error - отсутствует свойство z. В файле .js с // @ts-check отсутствующее z — это ожидаемая ошибка; в файле .ts типы JSDoc игнорируются, поэтому компилятор вместо этого сообщает, что директива не использована.
Вывод: программа на TypeScript и захваченный вывод
// @ts-check
/** @typedef {{ x: number, y: number }} Point */
/**
* @typedef {Point & { z: number }} Point3D
*/
/** @type {Point3D} */
const point3d = { x: 1, y: 2, z: 3 };
// @ts-expect-error - отсутствует свойство z
const point2d = { x: 1, y: 2 };
Типы функций
JSDoc может описать каждую часть функции: её параметры, возвращаемое значение, типы колбэков и даже несколько перегруженных сигнатур.
Объявления функций
Каждый @param задаёт параметру тип и короткое описание; @returns делает то же самое для результата.
// @ts-check напишите calculateArea(width, height), задокументированную с помощью @param {number} для обоих параметров (с короткими описаниями) и @returns {number} Вычисленная площадь, возвращающую произведение. Сохраните calculateArea(10, 20) в area. Ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод
// @ts-check
/**
* Вычисляет площадь прямоугольника
* @param {number} width - Ширина прямоугольника
* @param {number} height - Высота прямоугольника
* @returns {number} Вычисленная площадь
*/
function calculateArea(width, height) {
return width * height;
}
// TypeScript знает типы параметров и возвращаемого значения
const area = calculateArea(10, 20);
Функциональные выражения и колбэки
@callback определяет переиспользуемый тип функции. Затем его можно использовать с @type для стрелочной функции или как тип параметра.
@callback для типизации функциональных выражений и параметров-колбэков в JSDoc.
Определите тип колбэка StringProcessor, принимающий string и возвращающий string. Объявите toUpperCase с @type {StringProcessor} как стрелочную функцию, затем функцию processStrings(strings, processor), задокументированную с помощью @param {string[]}, @param {StringProcessor} и @returns {string[]}, которая применяет обработчик к массиву через map. Вызовите её с ["привет", "мир"] и toUpperCase. Ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод
// @ts-check
/**
* @callback StringProcessor
* @param {string} input
* @returns {string}
*/
/**
* @type {StringProcessor}
*/
const toUpperCase = (str) => str.toUpperCase();
/**
* @param {string[]} strings
* @param {StringProcessor} processor
* @returns {string[]}
*/
function processStrings(strings, processor) {
return strings.map(processor);
}
const result = processStrings(['привет', 'мир'], toUpperCase);
// result будет ['ПРИВЕТ', 'МИР']
Перегрузки функций
Несколько блоков @overload дают одной функции больше одной сигнатуры. Последний блок описывает реализацию, которая обрабатывает их все.
@overload в JSDoc, которые дают одной функции две типизированные сигнатуры.
Напишите add(a, b) с двумя блоками @overload — строка плюс строка возвращает строку, число плюс число возвращает число — а затем сигнатуру реализации с string | number. Если хотя бы один аргумент является строкой, верните конкатенацию через String(), иначе — числовую сумму. Вызовите её как add("Привет, ", "Мир!") и add(10, 20). Ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод
// @ts-check
/**
* @overload
* @param {string} a
* @param {string} b
* @returns {string}
*/
/**
* @overload
* @param {number} a
* @param {number} b
* @returns {number}
*/
/**
* @param {string | number} a
* @param {string | number} b
* @returns {string | number}
*/
function add(a, b) {
if (typeof a === 'string' || typeof b === 'string') {
return String(a) + String(b);
}
return a + b;
}
const strResult = add('Привет, ', 'Мир!'); // строка
const numResult = add(10, 20); // число
Продвинутые типы
Более продвинутые возможности TypeScript — объединения, пересечения, дженерики, отображаемые и условные типы — тоже доступны через JSDoc.
Типы-объединения и типы-пересечения
& объединяет типы, а | разрешает один из нескольких типов. Проверка in сужает объединение внутри функции.
@typedef в JSDoc.
Определите Person (name, age), Employee как Person & { employeeId: string } и Visitor как Person | { guestId: string, visitDate: Date }. Объявите employee (Alice, 30, E123) и guest (G456, new Date()), затем функцию getVisitorId(visitor), которая с помощью проверки "guestId" in visitor возвращает либо guestId, либо name. Ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод
// @ts-check
/** @typedef {{ name: string, age: number }} Person */
/** @typedef {Person & { employeeId: string }} Employee */
/** @typedef {Person | { guestId: string, visitDate: Date }} Visitor */
/** @type {Employee} */
const employee = {
name: 'Alice',
age: 30,
employeeId: 'E123'
};
/** @type {Visitor} */
const guest = {
guestId: 'G456',
visitDate: new Date()
};
/**
* @param {Visitor} visitor
* @returns {string}
*/
function getVisitorId(visitor) {
if ('guestId' in visitor) {
return visitor.guestId; // TypeScript знает, что это гость
}
return visitor.name; // TypeScript знает, что это Person
}
Отображаемые и условные типы
@template делает typedef обобщённым. Тело typedef может затем использовать отображаемые типы, переименование ключей и условные типы точно так же, как файл .ts.
@template в JSDoc может выражать отображаемые и условные типы.
Определите обобщённый typedef MethodNames<T>, который оставляет только те ключи T, значения которых являются функциями, и обобщённый отображаемый тип Getters<T>, который переименовывает каждый ключ K в `get${Capitalize<K>}` с типом () => T[K]. Объявите userGetters с типом Getters<{ name: string, age: number }>, где getName возвращает "John", а getAge возвращает 30, и прочитайте оба значения в name и age. Добавьте export {}, чтобы name не конфликтовало с глобальной переменной. Ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод
// @ts-check
/**
* @template T
* @typedef {{ [K in keyof T]: T[K] extends Function ? K : never }[keyof T]} MethodNames
*/
/**
* @template T
* @typedef {{
* [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
* }} Getters
*/
/** @type {Getters<{ name: string, age: number }>} */
const userGetters = {
getName: () => 'John',
getAge: () => 30
};
// TypeScript следит за соблюдением возвращаемых типов
const name = userGetters.getName(); // строка
const age = userGetters.getAge(); // число
// Делаем этот файл модулем, чтобы `name` не конфликтовало с глобальным `name`
export {};
Импорт типов
Тип JSDoc можно импортировать из другого файла или из пакета с помощью import('...') внутри @typedef. Для библиотек, которые не поставляются с типами, вы можете написать собственный файл объявлений (.d.ts).
Импорт типов в JSDoc
Три способа перенести тип в файл JavaScript: из локального файла TypeScript, из пакета в node_modules и под новым именем.
// @ts-check
// Importing types from TypeScript files
/** @typedef {import('./types').User} User */
// Importing types from node_modules
/** @typedef {import('express').Request} ExpressRequest */
// Importing with renaming
/** @typedef {import('./api').default as ApiClient} ApiClient */
Файл объявлений для модуля
Файл types.d.ts, который объявляет пакет my-module: интерфейс Config и две экспортируемые функции.
// types.d.ts
declare module 'my-module' {
export interface Config {
apiKey: string;
timeout?: number;
retries?: number;
}
export function initialize(config: Config): void;
export function fetchData<T = any>(url: string): Promise<T>;
}
Лучшие практики
Следуйте этим практикам, когда используете JSDoc вместе с TypeScript:
- Ставьте
// @ts-checkв начале каждого файла, который хотите проверять - Используйте
@typedefдля сложных типов, которые встречаются больше чем в одном месте - Документируйте все параметры функций и возвращаемые типы
- Используйте
@templateдля обобщённых функций и типов - Пишите файлы объявлений (
.d.ts) для сторонних библиотек без типов - Предпочитайте
@ts-expect-error, а не@ts-ignore, когда вы ожидаете ошибку
Частые ошибки
Обратите внимание на эти распространённые проблемы:
- Отсутствие
// @ts-check: без него (или безcheckJs) ничего не проверяется - Неверный синтаксис JSDoc: одна опечатка может отключить проверку типов для этого комментария
- Конфликты типов: типы из разных источников могут не совпадать
- Проблемы вывода типов: иногда TypeScript не может определить тип самостоятельно
- Производительность: большие файлы JavaScript со сложными типами могут проверяться медленно
Заключение
JSDoc вместе с TypeScript — это мощный способ добавить безопасность типов в проект на JavaScript, не переводя файлы в TypeScript.
Это особенно полезно для:
- Постепенного переноса кодовой базы JavaScript на TypeScript
- Добавления проверки типов в существующий проект на JavaScript
- Работы в средах, где файлы
.tsне поддерживаются - Документирования кода JavaScript информацией о типах
С помощью приёмов с этой страницы вы получаете многие преимущества TypeScript, продолжая писать на JavaScript.
Помните: для новых проектов или полного переноса файлы .ts по-прежнему дают лучший опыт работы с TypeScript.
Готовы начать? Добавьте // @ts-check в свои файлы JavaScript и начните добавлять аннотации типов JSDoc. Компилятор поймает ошибки до того, как они попадут в продакшн.