Урок 37: Интеграция с внешними библиотеками. Файлы .d.ts

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

До этого мы работали в TypeScript и система типов была нам полностью подконтрольна. Но реальность такова, что npm хранит в себе сотни тысяч библиотек, написанных на ванильном JavaScript. У них нет типов, они не знают о наших interface и type. И когда мы пытаемся импортировать такую библиотеку в наш строго типизированный проект, TypeScript закономерно начинает паниковать и сыпать ошибками Cannot find module или any. Наша задача сегодня научиться успокаивать его и получать все преимущества типизации даже при использовании «нетипизированного» кода.

Как использовать обычные JS-библиотеки в TS?

Давайте смоделируем реальную ситуацию. Представьте, что наш проект требует использования популярной библиотеки lodash, которая предоставляет множество полезных утилит для работы с данными. Мы устанавливаем ее через npm:

bash
npm install lodash

Затем в нашем TypeScript-файле мы пытаемся импортировать функцию capitalize и использовать ее.

main.ts

typescript
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.tslodash.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-разработчика. Механика работы проста до гениальности:

  1. Сообщество разработчиков пишет и поддерживает файлы .d.ts для различных библиотек.

  2. Эти файлы хранятся в репозитории DefinitelyTyped.

  3. Для публикации используется scope @types в npm. Это специальное пространство имен, зарезервированное для описаний типов.

  4. Когда вы хотите добавить типы для библиотеки, например, lodash, вы ищете пакет @types/lodash.

Как это использовать на практике? Очень просто! Для большинства популярных библиотек нужно всего лишь установить соответствующий пакет из @types.

Вернемся к нашему примеру с lodash. Чтобы исправить ошибку, мы устанавливаем типы:

bash
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

javascript
function makeCool(str) {
    return `😎 ${str} 😎`;
}
module.exports = { makeCool };

Мы устанавливаем ее и пытаемся импортировать в TS-коде:

main.ts

typescript
import { makeCool } from 'cool-library'; // Ошибка: Could not find a declaration file

Чтобы это исправить, создадим файл types/global.d.ts (папку types придется создать самостоятельно) и добавим в него объявление модуля.

types/global.d.ts

typescript
// Объявляем модуль для библиотеки 'cool-library'
declare module 'cool-library' {
    // Описываем функцию, которую она экспортирует
    export function makeCool(str: string): string;
}

Теперь нужно указать TypeScript, где искать наши собственные файлы с объявлениями. Для этого в tsconfig.json добавляем путь к папке types в поле typeRoots или просто убеждаемся, что она включена в includes.

tsconfig.json

json
{
  "compilerOptions": {
    ...
    "typeRoots": ["./node_modules/@types", "./types"] // TypeScript будет искать типы здесь
  },
  "include": ["src/**/*", "types/**/*"] // И здесь
}

После этого TypeScript подхватит наше объявление и перестанет ругаться на импорт. Функция makeCool теперь будет полностью типизирована: мы и автодополнение получим и проверку типов.

Бывает и так, что библиотека подключается через тег <script> в HTML и добавляет свою функциональность в глобальную область видимости (например, добавляет функцию $ или jQuery). Для таких случаев используются объявления в глобальной области видимости (declare global).

Допустим, у нас есть скрипт, который добавляет глобальную функцию showNotification.

index.html

html
<script src="https://example.com/notification.js"></script>
<script>
    showNotification('Hello!'); // Работает в JS
</script>

В TypeScript коде вызов showNotification('Hello!') вызовет ошибку, так как TS не знает о существовании такой глобальной функции. Мы можем объявить ее в нашем .d.ts файле:

types/global.d.ts

typescript
// Объявляем функцию в глобальной области видимости
declare function showNotification(message: string, duration?: number): void;

// Если библиотека добавляет что-то более сложное, например, объект
declare global {
  interface Window {
    MySuperLibrary: {
      init: (config: object) => void;
    };
  }
}

Этих знаний уже достаточно, чтобы начать интегрировать практически любую JS-библиотеку в ваш проект. Давайте закрепим все это на практике.

Практические задачи и примеры кода

Задача 1: Интеграция библиотеки date-fns

Библиотека date-fns предоставляет множество полезных функций для работы с датами. Несмотря на то, что она написана на JS, у нее есть отличные типы.

  1. Установите саму библиотеку и ее типы.

    bash
    npm install date-fns
    npm install -D @types/date-fns
  2. Напишите код, который использует функцию formatDistance для вывода читаемого расстояния между двумя датами. Убедитесь, что автодополнение работает и подсказывает вам аргументы.

main.ts

typescript
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

javascript
export function add(a, b) {
    return a + b;
}
export function multiply(a, b) {
    return a * b;
}

Теперь попробуйте импортировать и использовать его в вашем main.ts.

main.ts

typescript
import { add, multiply } from './libs/simple-calculator'; // Будет ошибка без типов

console.log(add(5, 3));

Ваша задача создать файл объявлений types/simple-calculator.d.ts, который опишет эту библиотеку и подключить его.

types/simple-calculator.d.ts

typescript
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

typescript
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

typescript
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». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.

Персональные рекомендации
Оставить комментарий