Вы решили, что собираетесь использовать Babel и писать исходный код на TypeScript. Но что происходит, когда вы разделяете типы TS через границы модуля? Как Babel должен обрабатывать этот импорт и экспорт?

Давайте кратко рассмотрим, как Babel и TypeScript работают вместе.

TypeScript делает две вещи:

  1. Добавляет статическую проверку типа в код, который вы обычно писали бы как JavaScript.
  2. Транспортирует код TS + JS во множество разновидностей JS.

Вавилонский также делает вторую вещь. Подход Babel (в частности, плагин transform-typescript) состоит в том, чтобы просто удалить типы, а затем преобразовать их. Это позволяет вам использовать Babel со всеми его преимуществами, сохраняя при этом возможность кормить его ts файлами.

Babel собирается удалить (elide) любые import объявления, которые используются только как типы.

Источник:

// example.ts
import { Color } from "./types";
const changeColor = (color: Color) => {
  window.color = color;
};

Транспортированный вывод Babel:

// example.js
const changeColor = (color) => {
  window.color = color;
};

Babel с уверенностью может удалить это объявление, проанализировав только этот отдельный файл. Проблемы возникают, когда Babel не может знать, является ли конкретный импорт типом, который следует удалить, или фактическим значением, которое следует сохранить. Это может произойти при использовании реэкспорта.

// example.ts
import { Color } from "./types";
export { Color };

Здесь Бабель не может сказать, глядя на example.ts, что Color на самом деле тип. Babel будет вынужден неправильно оставить это объявление в транслируемом выводе.

Почему это происходит? Что ж, Babel явно обрабатывает один файл за раз во время процесса транспиляции. Предположительно, команда babel не хочет встраивать тот же процесс разрешения типов, что и TypeScript, просто чтобы удалить эти типы.

Enter, isolatedModules

isolatedModules - параметр компилятора TypeScript, предназначенный для защиты. Проверка типов, предоставляемая tsc при включенном флаге isolatedModulescompiler, будет сообщать об ошибках типа, которые, если их не устранить, могут повлиять на инструменты транспиляции (babel), которые обрабатывают файлы изолированно.

Из Документов TypeScript:

Выполните дополнительные проверки, чтобы убедиться, что отдельная компиляция (например, с transpileModule или @ babel / plugin-transform-typescript) будет безопасной.

Из babel docs:

--isolatedModules Это поведение Babel по умолчанию, и его нельзя отключить, потому что Babel не поддерживает межфайловый анализ.

Другими словами, каждый ts-файл должен быть способен переноситься отдельно. Флаг isolatedModules не позволяет нам включать неоднозначно разрешенный импорт.

Изолированные модули на примере

Рассмотрение пары примеров того, как babel транспилирует код, демонстрирует важность флага isolatedModules.

Оба этих примера основаны на сборке клиента api для потоковой передачи музыки.

Пример 1 - Неоднозначный реэкспорт

Здесь мы берем типы, которые мы определили в types.ts файле, а затем повторно экспортируем их из lib-ambiguous-re-export.ts. Этот код не пройдет проверку типа, если включен isolatedModules.

Источник:

// src/types.ts
export type Playlist = {
  id: string;
  name: string;
  trackIds: string[];
};
export type Track = {
  id: string;
  name: string;
  artist: string;
  duration: number;
};
// src/lib-ambiguous-re-export.ts
export { Playlist, Track } from "./types";
export { CreatePlaylistRequestParams, createPlaylist } from "./api";

Транспортированный вывод Babel:

// dist/types.js
--empty--
// dist/lib-ambiguous-re-export.js
export { Playlist, Track } from "./types";
export { CreatePlaylistRequestParams, createPlaylist } from "./api";

Ошибка в источнике:

Несколько идей:

  • Babel удаляет все из нашего typesmodule, потому что он содержит только типы, которые не используются в JS-land.
  • Babel не вносил никаких изменений в наш lib модуль. Это нехорошо. Playlist и Track должны быть удалены Babel. Запуск этого кода через узел не сработает. Процесс разрешения модуля Node увидит, когда встретит строку реэкспорта в lib-ambiguous-re-export.js, что ничего не экспортируется из types.js, и процесс потерпит неудачу.
  • Как показано на снимке экрана VSCode, процесс проверки типа tsc немедленно сообщает об этих неоднозначных реэкспортах как об ошибках.

Пример 2 - Явный импорт, явный экспорт типа

На этот раз мы явно говорим о повторном экспорте типов в lib-import-export.ts. Этот код будет пройти проверку типа, когда включен isolatedModules.

Источник:

// src/types.ts
Same as first example
// src/lib-import-export.ts
import {
  Playlist as PlaylistType,
  Track as TrackType,
} from "./types";
import {
  CreatePlaylistRequestParams as CreatePlaylistRequestParamsType,
  createPlaylist
} from "./api";
export type Playlist = PlaylistType;
export type Track = TrackType;
export type CreatePlaylistRequestParams = CreatePlaylistRequestParamsType;
export { createPlaylist };

Транспортированный вывод Babel:

// dist/types.js
--empty-- TODO or does babel remove it all together?
// dist/lib-import-export.js
import { createPlaylist } from "./api";
export { createPlaylist };

Пара идей:

  • Babel по-прежнему выводит пустой types.js файл. Но это нормально, потому что наш транспилированный lib-import-export.js больше на него не ссылается.
  • lib-import-export.js теперь экспортирует только функцию JS, createPlaylist. Типов не существует. Это связано с дополнительной работой, которую мы вложили в исходный код, импортировав наши типы, а затем явно повторно экспортируя эти типы.
  • tsc доволен этим, поэтому вы не увидите, как он сообщает об ошибках, как в первом примере.

Вы можете видеть важность isolatedModules при обработке файлов TypeScript с помощью Babel. Первый пример проще и лаконичнее. Но это нарушает изолированные модули. До сих пор, чтобы соответствовать, нужно использовать что-то вроде подробного второго примера.

Решено TypeScript 3.8

TypeScript 3.8 представляет новый синтаксис, который при использовании добавляет уверенности процессу разрешения типов. Теперь компилятор (будь то tsc, babel или что-то еще) сможет просмотреть отдельный файл и исключить импорт или экспорт, если это тип TypeScript.

Новый синтаксис, который позволяет это сделать, не вводит никаких новых ключевых слов, но позволяет использовать type в двух новых местах.

import type ... from — Сообщает компилятору, что то, что вы импортируете, определенно является типом и не требуется (или не требуется) в построенном выводе.

export type ... from - То же, но для реэкспорта.

Поскольку у нас было два приведенных выше примера, подчеркивающих необходимость чего-то лучшего, вот как мы реорганизуем этот код для использования в TS 3.8.

Источник:

// src/lib-type-re-export.ts
export type { Track, Playlist } from "./types";
export type { CreatePlaylistRequestParams } from "./api";
export { createPlaylist } from "./api";

Транспортированный вывод Babel:

// dist/lib-type-re-export.js
export { createPlaylist } from "./api";

Еще два вывода:

  • Исходный код очень похож на пример 1 изолированных модулей, но двусмысленность удалена из реэкспорта.
  • Хорошо, так что это на самом деле не переносимый вывод babel, поскольку babel пока не может обрабатывать импорт и экспорт нового типа TS 3.8. Но когда выйдет Babel 7.9, это то, чем должно быть.

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

Соответствующие ресурсы для получения дополнительной информации об импорте / экспорте только типов: