Сегодня нас ждет крайне важнаятема, с которой сталкивается абсолютно каждый разработчик на TS, как только выходит за пределы чистого кода. Мы поговорим о том, как заставить TypeScript мирно и продуктивно сосуществовать с огромной экосистемой обычных JavaScript-библиотек.
До этого мы работали в TypeScript и система типов была нам полностью подконтрольна. Но реальность такова, что npm хранит в себе сотни тысяч библиотек, написанных на ванильном JavaScript. У них нет типов, они не знают о наших interface и type. И когда мы пытаемся импортировать такую библиотеку в наш строго типизированный проект, TypeScript закономерно начинает паниковать и сыпать ошибками Cannot find module или any. Наша задача сегодня научиться успокаивать его и получать все преимущества типизации даже при использовании «нетипизированного» кода.
Как использовать обычные JS-библиотеки в TS?
Давайте смоделируем реальную ситуацию. Представьте, что наш проект требует использования популярной библиотеки lodash, которая предоставляет множество полезных утилит для работы с данными. Мы устанавливаем ее через npm:
npm install lodash
Затем в нашем TypeScript-файле мы пытаемся импортировать функцию capitalize и использовать ее.
main.ts
import { capitalize } from 'lodash'; const name = 'maxim'; const capitalizedName = capitalize(name); console.log(capitalizedName);
Казалось бы, все должно работать. Но вместо этого компилятор TypeScript выдает нам ошибку:
Could not find a declaration file for module 'lodash'. ... implicitly has an 'any' type.
Что происходит? TypeScript пытается найти информацию о типах для модуля lodash. Он ищет файл с объявлениями типов, обычно это index.d.ts, lodash.d.ts или что-то подобное. Поскольку сама библиотека написана на JavaScript, таких файлов в ее папке node_modules/lodash нет. Следовательно, TypeScript не может проверить, правильно ли мы используем функцию capitalize: какие аргументы она принимает, что возвращает. Для него этот импорт становится неким «черным ящиком» с типом any, что нарушает все наши принципы строгой типизации.
Эта проблема, фундаментальный барьер между миром JS и TS. Но создатели TypeScript прекрасно знали о ее существовании и предложили изящное и масштабируемое решение. Оно построено на простой идее: описания типов можно написать отдельно от самой реализации. То есть нам не нужно переписывать всю библиотеку lodash на TypeScript, чтобы получить автодополнение и проверку типов. Достаточно создать один-единственный файл, который будет описывать что делают функции библиотеки, не описывая как они это делают. Этот файл и есть файл объявлений TypeScript, который имеет расширение .d.ts.
Такой подход гениален. Он позволяет сообществу развивать типы для существующих JS-библиотек независимо от их авторов. И именно этим сообществом является проект DefinitelyTyped.
Обзор @types DefinitelyTyped
DefinitelyTyped это гигантский репозиторий на GitHub, в котором хранятся описания типов для тысяч популярных JavaScript-библиотек. Это настоящая сокровищница для TypeScript-разработчика. Механика работы проста до гениальности:
-
Сообщество разработчиков пишет и поддерживает файлы
.d.tsдля различных библиотек. -
Эти файлы хранятся в репозитории DefinitelyTyped.
-
Для публикации используется scope
@typesв npm. Это специальное пространство имен, зарезервированное для описаний типов. -
Когда вы хотите добавить типы для библиотеки, например,
lodash, вы ищете пакет@types/lodash.
Как это использовать на практике? Очень просто! Для большинства популярных библиотек нужно всего лишь установить соответствующий пакет из @types.
Вернемся к нашему примеру с lodash. Чтобы исправить ошибку, мы устанавливаем типы:
npm install -D @types/lodash
Обратите внимание на флаг -D (или --save-dev). Это важно! Пакеты с типами нужны только на этапе разработки и компиляции. В рантайме, в вашем собранном JavaScript-бандле, они не используются. После установки мы можем перезапустить наш IDE или сервис языка TypeScript (часто это кнопка «Restart TS Server» в VS Code). И ошибка пропадает.
Теперь, когда мы начинаем писать capitalize(, среда разработки подсказывает нам сигнатуру функции: мы видим, что она принимает string: string и возвращает string. TypeScript теперь может проверять, правильное ли количество аргументов мы передаем и правильного ли они типа. Мы получили полную TypeScript-интеграцию, не меняя ни строчки кода в самой библиотеке lodash.
Как найти типы для нужной библиотеки? Обычно достаточно зайти на ее страницу на npmjs.com. Например, на странице lodash справа есть раздел «Types», где есть ссылка на @types/lodash. Если пакет с типами существует, вы найдете его там.
Но что делать, если для нужной вам библиотеки нет готовых типов в @types? Не отчаиваться, а брать дело в свои руки и объявлять типы самостоятельно! Именно об этом наш следующий раздел.
Объявление модулей
Бывают ситуации, когда библиотека настолько новая, нишевая или маленькая, что для нее еще никто не создал типы в DefinitelyTyped. Или же вы написали свою собственную маленькую JS-библиотечку и хотите использовать ее в TS-проекте. В этом случае нам нужно самостоятельно рассказать TypeScript о том, что представляет из себя этот модуль.
Для этого используется конструкция declare module. Мы создаем файл с расширением .d.ts (например, types/global.d.ts) и в нем объявляем модуль по имени той библиотеки, которую мы импортируем.
Давайте рассмотрим практический пример. Представьте, что у нас есть простая JS-библиотека cool-library, которая экспортирует одну функцию makeCool, принимающую строку и возвращающую ее в «крутом» виде (добавляет смайлики, например).
node_modules/cool-library/index.js
function makeCool(str) { return `😎 ${str} 😎`; } module.exports = { makeCool };
Мы устанавливаем ее и пытаемся импортировать в TS-коде:
main.ts
import { makeCool } from 'cool-library'; // Ошибка: Could not find a declaration file
Чтобы это исправить, создадим файл types/global.d.ts (папку types придется создать самостоятельно) и добавим в него объявление модуля.
types/global.d.ts
// Объявляем модуль для библиотеки 'cool-library' declare module 'cool-library' { // Описываем функцию, которую она экспортирует export function makeCool(str: string): string; }
Теперь нужно указать TypeScript, где искать наши собственные файлы с объявлениями. Для этого в tsconfig.json добавляем путь к папке types в поле typeRoots или просто убеждаемся, что она включена в includes.
tsconfig.json
{ "compilerOptions": { ... "typeRoots": ["./node_modules/@types", "./types"] // TypeScript будет искать типы здесь }, "include": ["src/**/*", "types/**/*"] // И здесь }
После этого TypeScript подхватит наше объявление и перестанет ругаться на импорт. Функция makeCool теперь будет полностью типизирована: мы и автодополнение получим и проверку типов.
Бывает и так, что библиотека подключается через тег <script> в HTML и добавляет свою функциональность в глобальную область видимости (например, добавляет функцию $ или jQuery). Для таких случаев используются объявления в глобальной области видимости (declare global).
Допустим, у нас есть скрипт, который добавляет глобальную функцию showNotification.
index.html
<script src="https://example.com/notification.js"></script> <script> showNotification('Hello!'); // Работает в JS </script>
В TypeScript коде вызов showNotification('Hello!') вызовет ошибку, так как TS не знает о существовании такой глобальной функции. Мы можем объявить ее в нашем .d.ts файле:
types/global.d.ts
// Объявляем функцию в глобальной области видимости declare function showNotification(message: string, duration?: number): void; // Если библиотека добавляет что-то более сложное, например, объект declare global { interface Window { MySuperLibrary: { init: (config: object) => void; }; } }
Этих знаний уже достаточно, чтобы начать интегрировать практически любую JS-библиотеку в ваш проект. Давайте закрепим все это на практике.
Практические задачи и примеры кода
Задача 1: Интеграция библиотеки date-fns
Библиотека date-fns предоставляет множество полезных функций для работы с датами. Несмотря на то, что она написана на JS, у нее есть отличные типы.
-
Установите саму библиотеку и ее типы.
npm install date-fns npm install -D @types/date-fns
-
Напишите код, который использует функцию
formatDistanceдля вывода читаемого расстояния между двумя датами. Убедитесь, что автодополнение работает и подсказывает вам аргументы.
main.ts
import { formatDistance, subDays } from 'date-fns'; const date = subDays(new Date(), 3); const result = formatDistance(date, new Date(), { addSuffix: true }); console.log(result); // "3 days ago"
Задача 2: Создание собственных объявлений для небольшой библиотеки
Создайте файл simple-calculator.js и поместите его в папку src/libs.
src/libs/simple-calculator.js
export function add(a, b) { return a + b; } export function multiply(a, b) { return a * b; }
Теперь попробуйте импортировать и использовать его в вашем main.ts.
main.ts
import { add, multiply } from './libs/simple-calculator'; // Будет ошибка без типов console.log(add(5, 3));
Ваша задача создать файл объявлений types/simple-calculator.d.ts, который опишет эту библиотеку и подключить его.
types/simple-calculator.d.ts
declare module './libs/simple-calculator' { export function add(a: number, b: number): number; export function multiply(a: number, b: number): number; }
Не забудьте добавить папку types в includes вашего tsconfig.json. После этого ошибка должна исчезнуть.
Задача 3: Работа с библиотекой без типов (продвинутый уровень)
Представьте, что вы нашли библиотеку dangerous-legacy-api@1.0.0, которая не имеет типов в @types. Вы смотрите ее документацию и видите, что она экспортирует объект api с двумя методами:
-
api.getData(key: string, callback: (error: Error, data: unknown) => void): void -
api.postData(key: string, data: object): Promise<void>
Создайте для нее файл объявлений types/dangerous-legacy-api.d.ts.
types/dangerous-legacy-api.d.ts
declare module 'dangerous-legacy-api' { export interface ApiResponse { // Здесь вы можете описать структуру ожидаемого ответа, если она известна id: number; value: string; } export const api: { getData: (key: string, callback: (error: Error | null, data: ApiResponse) => void) => void; postData: (key: string, data: object) => Promise<void>; }; }
Теперь вы можете использовать эту библиотеку с типами:
main.ts
import { api } from 'dangerous-legacy-api'; api.getData('user_123', (err, data) => { if (err) { console.error(err); return; } // Теперь `data` имеет тип ApiResponse, с ним можно работать безопасно! console.log(data.value.toUpperCase()); // Автодополнение работает! }); api.postData('new_data', { name: 'Maxim' }).then(() => console.log('Success!'));
Заключение
Вы только что освоили один из ключевых навыков для профессиональной работы с TypeScript, интеграцию с внешним JavaScript-миром. Теперь вы не ограничены только теми библиотеками, что написаны на TS. Вы можете с легкостью использовать практически любой npm-пакет, будь у него типы в @types или нет.
Мы разобрали, как решить проблему отсутствия типов у JS-библиотек, познакомились с огромным сообществом DefinitelyTyped и механикой работы @types/* пакетов. А главное, вы научились самостоятельно объявлять типы для модулей через declare module, что делает вас абсолютно независимыми в этом вопросе.
Этот навык станет вашим надежным спутником в любом реальном проекте. Не бойтесь заглядывать в исходники @types пакетов на GitHub, чтобы подсматривать, как более опытные разработчики описывают сложные API. Это отличный способ обучения.
Как всегда, жду ваши вопросы и успехи в комментариях.
Полный курс с уроками по TypeScript для начинающих вы можете найти на моем сайте: https://max-gabov.ru/typescript-dlya-nachinaushih
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


