Перейти к содержимому
Теория · Проект на TypeScript · урок 7 из 10

Файлы .d.ts и @types

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

Библиотека на JavaScript — такой прибор. В ней есть функции, но нет типов, и tsc не знает, какие аргументы им передавать. Возьмём популярную библиотеку lodash и её функцию chunk, которая режет массив на части заданного размера. Сначала библиотеку ставят командой npm, а npm кладёт пакеты в папку node_modules проекта:

TEXT
$ npm install lodash

Теперь импортируем chunk:

TypeScript
import { chunk } from "lodash"; // ✗

console.log(chunk([1, 2, 3, 4, 5], 2));
TEXT
error TS7016: Could not find a declaration file for module 'lodash'. '.../node_modules/lodash/lodash.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/lodash` if it exists or add a new declaration (.d.ts) file containing `declare module 'lodash';`

Давайте разберём сообщение:

  1. tsc нашёл lodash в папке node_modules, но там только JavaScript, а описания типов нет. Вместо точек tsc печатает полный путь к папке проекта.
  2. Без описания все функции библиотеки получили бы тип any, а строгий режим этого не разрешает.
  3. Вторая строка подсказывает два выхода: поставить пакет @types/lodash или написать файл объявлений самому.

Файл объявлений

Файл объявлений — это файл с расширением .d.ts, в котором есть только типы: имена функций, их параметры и результаты, но ни одного тела. Допустим, в проекте остался старый format.js на JavaScript с функцией formatPrice(price). Рядом с ним кладут format.d.ts:

TypeScript
export declare function formatPrice(price: number): string;

Разберём строку по частям:

  • declare значит «такая функция есть, но её код в другом месте»: тела нет, и строка кончается точкой с запятой;
  • export говорит, что функцию экспортирует модуль format.js;
  • (price: number): string — та самая надпись на кнопке: что передать и что получить.

Теперь main.ts пишет import { formatPrice } from "./format.js" и получает типы из format.d.ts, а при запуске Node.js выполняет format.js. Вызов formatPrice("100") компилятор остановит ошибкой TS2345, как будто функция написана на TypeScript.

Помните lib.dom.d.ts из первого урока темы? Это тоже файл объявлений: в нём описаны имена браузера, а их код живёт в самом браузере.

Пакеты @types

Для популярных библиотек объявления уже написаны. Сообщество собирает их в репозитории DefinitelyTyped и публикует отдельными пакетами @types/<имя>:

TEXT
$ npm install --save-dev @types/lodash

Что важно знать о пакетах @types:

  • tsc находит их сам в папке node_modules/@types: код по-прежнему пишет from "lodash", а импортировать из @types/lodash ничего не нужно;
  • после установки chunk([1, 2, 3, 4, 5], "2") не скомпилируется: размер части — число, а "2" — строка;
  • многие библиотеки везут свои файлы .d.ts в самом пакете, например axios, и пакет @types им не нужен;
  • флаг --save-dev ставит пакет только для разработки: типы нужны tsc, а не программе при запуске.

Откуда берутся пакеты @types и как TypeScript их находит, часто спрашивают на собеседовании. Ответ: их пишет сообщество в DefinitelyTyped, а tsc сам находит нужный пакет в node_modules/@types, когда код импортирует библиотеку; из @types ничего не импортируют.

declare module

Бывает, что типов нет ни в пакете, ни в @types. Например, у маленькой внутренней библиотеки компании tiny-slug, которая превращает заголовок в кусок адреса страницы. Тогда объявления пишут сами, в своём файле .d.ts, например src/types.d.ts:

TypeScript
declare module "tiny-slug" {
    export function slug(text: string): string;
}

declare module "tiny-slug" описывает весь пакет, а внутри фигурных скобок — то, что он экспортирует. Файл лежит в src и попадает в проверку вместе с остальными, поэтому import { slug } from "tiny-slug" получает типы, и slug(42) уже не скомпилируется.

Короткую запись подсказывает само сообщение TS7016: declare module "tiny-slug"; объявляет пакет без подробностей, и всё, что из него импортируют, получит тип any. Это быстрый выход, но проверок для такого пакета не будет.

Здесь аналогия с инструкцией перестаёт работать. Настоящую инструкцию пишет производитель прибора. Объявления может написать кто угодно, и tsc им верит: ошибётесь в .d.ts — tsc пропустит неверный вызов.

Частая ошибка

Импортировали функцию из пакета с типами:

TypeScript
import { chunk } from "@types/lodash"; // ✗
TEXT
error TS6137: Cannot import type declaration files. Consider importing 'lodash' instead of '@types/lodash'.

В @types/lodash лежит только описание, кода библиотеки там нет. Импортируйте из самой библиотеки, from "lodash", а типы tsc возьмёт из @types сам.

Главное

  • Файл .d.ts описывает JavaScript без типов: в нём только объявления с declare, без тел.
  • Пакеты @types/<имя> из DefinitelyTyped tsc находит в node_modules/@types сам, импортировать из них не нужно.
  • Библиотеке, которая везёт свои файлы .d.ts, пакет @types не нужен.
  • Для пакета без типов объявления пишут сами: declare module "имя" { … } в своём файле .d.ts.