Элемент import можно использовать только в файлах typescript ts 8002
Here, My primary question is what is "esModuleInterop": true, and "allowSyntheticDefaultImports": true, . I know they are sort of dependent on the "module": "commonjs", . Can someone try to explain it in the best human language possible?
The official docs for allowSyntheticDefaultImports states
Allow default imports from modules with no default export. This does not affect code emit, just typechecking.
What does that mean? If there isn't any export default then I think the only use case of the import default would be to initialize something? Like a singleton?
And --esModuleInterop definition on the compiler page
Emit __importStar and __importDefault helpers for runtime babel ecosystem compatibility and enable --allowSyntheticDefaultImports for typesystem compatibility.
Also seemed difficult for me to understand/comprehend
Общие вопросы
2 Answers 2
Problem occurs when we want to import CommonJS module into ES6 module codebase.
Before these flags we had to import CommonJS modules with star ( * as something ) import:
We can see that * was somehow equivalent to exports variable. It worked fine, but it wasn't compliant with es6 modules spec. In spec, the namespace record in star import ( moment in our case) can be only a plain object, not callable ( moment() is not allowed).
With flag esModuleInterop we can import CommonJS modules in compliance with es6 modules spec. Now our import code looks like this:
It works and it's perfectly valid with es6 modules spec, because moment is not namespace from star import, it's default import.
But how does it work? As you can see, because we did a default import, we called the default property on a moment object. But we didn't declare a default property on the exports object in the moment library. The key is the __importDefault function. It assigns module ( exports ) to the default property for CommonJS modules:
As you can see, we import es6 modules as they are, but CommonJS modules are wrapped into an object with the default key. This makes it possible to import defaults on CommonJS modules.
__importStar does the similar job - it returns untouched esModules, but translates CommonJS modules into modules with a default property:
And what about allowSyntheticDefaultImports - what is it for? Now the docs should be clear:
Allow default imports from modules with no default export. This does not affect code emit, just typechecking.
In moment typings we don't have specified default export, and we shouldn't have, because it's available only with flag esModuleInterop on. So allowSyntheticDefaultImports will not report an error if we want to import default from a third-party module which doesn't have a default export.
Я довольно долго работаю с typescript, и у меня было много проблем с тем, чтобы разобраться с его модулями и советующими настройками, и должен сказать, вокруг них и вправду много непонятного. Пространства имен, import * as React from 'react' , esModuleInterop и т.д. Поэтому давайте разберемся из-за чего поднялась вся шумиха.
Я не буду говорить о пространствах имен как о модульной системе в typescript, поскольку идея оказалась не лучшей (особенно учитывая текущий вектор развития), и этим никто сейчас не пользуется.
Итак, как же обстояли дела до появления esModuleInterop ? Были почти все те же модули, что есть у babel или браузеров, а также именованные импорты/экспорты. Однако, в вопросах экспортов и импортов по умолчанию у typescript был свой собственный вариант: нужно было писать import * as React from 'react' (вместо import React from 'react' ), и, конечно, здесь речь не только о react, а обо всех импортах по умолчанию из commonjs . Как так вышло?
Чтобы в этом разобраться, давайте посмотрим, как работает совместимость между некоторыми паттернами в модулях commonjs и es6 . Например, у нас есть модуль, который экспортирует foo и bar в качестве ключей:
Мы можем сделать импорт с помощью require и деструктуризации:
И применить тот же принцип, используя именованный импорт (хотя, если по-честному, то это не деструктуризация):
Однако более распространенный паттерн в commonjs – это const myModule = require('my-module') (потому, что деструктуризации еще не было), но как сделать это же в es6 ?
При разработке спецификации для импорта в es6 одним из важных аспектов была совместимость с commonjs , так как на commonjs было уже написано много кода. Вот так и появились импорт и экспорт по умолчанию. Да, единственной целью было обеспечивать совместимость с commonjs , чтобы мы могли писать import myModule from 'my-module и получать ровно тот же результат. Однако из спецификаций это было неочевидно, и к тому же, реализация совместимости была прерогативой разработчиков транспайлера. И вот тут как раз и случился великий раскол: import React from 'react' или же import * as React from 'react' – вот в чем вопрос.
Почему typescript выбрал последнее? Поставьте себя на место разработчика транспайлера и спросите себя, как можно легче всего транспилировать импорты из es6 в commonjs ? Допустим, у вас есть следующий набор импортов и экспортов:
Итак, будем использовать объект js с ключом default для экспорта по умолчанию!
Круто, но как насчет совместимости? Если импорт по умолчанию означает, что мы возьмем поле с именем default , значит когда мы напишем import React from 'react' – это будет значить const < default: React >= require('react') , но так не работает! Тогда вместо этого попробуем использовать импорт со звездочкой. Теперь пользователям придется писать import * as React from 'react' , чтобы добраться до содержимого module.exports .
Однако здесь есть семантическое отличие от commonjs .
Commonjs был как обычный javascript, не больше. Просто функции и объекты, без всяких require. С другой стороны, в импорте es6 , require сейчас часть спецификации, поэтому myModule в данном случае – это не просто обычный объект javascript, а то, что зовется пространством имен (не путать с namespaces в typescript), которое, соответственно, обладает определенными свойствами. Одно из них заключается в том, что пространство имен нельзя вызвать. И в чем же тут проблема, вы можете спросить?
Давайте опробуем другой паттерн commonjs , с одной функцией в качестве экспорта:
Мы можем воспользоваться require и выполнить ее:
Хотя если попытаетесь выполнить это в spec-complaint среде с модулями ES6, то получите ошибку:
Все потому, что пространство имен – это не то же самое, что объект javascript, а отдельная структура, хранящая каждый экспорт es6.
Но вот Babel понял все правильно и предоставил такой вариант совместимости, при котором мы можем написать import React from 'react ' и это будет работать. При транспиляции он помечает каждый модуль es6 специальным флагом в module.exports , чтобы мы понимали, что если флаг истинный, то возвращается module.exports , а если ложный (например, если это библиотека commonjs , которая не была транспилирована), то нам нужно будет обернуть текущий экспорт в < default: export >, чтобы мы могли каждый раз использовать default (взгляните исправляется автоматически), TypeScript 3 позволит вам быстро преобразовать импорт со звездочками к стандартному.
Поэтому я действительно выступаю за использование опции esModuleInterop , хотя бы потому что она не только позволят вам писать меньше кода и облегчает его чтение (и это не просто слова, например, rollup не позволит вам так использовать импорты со звездочками), но и уменьшает разногласия между сообществами typescript и babel.
Предостережение: раньше существовала опция enableSyntheticDefaultImports , которая затыкала рот компилятору, когда он пытался пожаловаться на неправильный импорт по умолчанию, поэтому нам понадобился собственный способ обеспечивать совместимость с commonjs (например, WebpackDefaultImportPlugin ), но это было проблемно, поскольку, например, если у вас есть тесты, то вам все еще нужно обеспечивать такую совместимость. Обратите внимание, что esModuleInterop включает синтетический импорт по умолчанию только в случае, если ваш цель
Надеюсь, мое объяснение хоть немного прояснило ситуацию, но если у вас остались вопросы, вы можете задать мне их в twitter!
Обратите внимание: для большого удобства в изучении книга была оформлена в виде прогрессивного веб-приложения.
В TS , как и в ECMAScript2015 , любой файл, содержащий import или export верхнего уровня (глобальный), считается модулем.
Файл, не содержащий указанных ключевых слов, является глобальным скриптом.
Модули выполняются в собственной области видимости, а не в глобальной. Это означает, что переменные, функции, классы и т.д., объявленные в модуле, недоступны за пределами модуля до тех пор, пока они в явном виде не будут из него экспортированы. Кроме того, перед использованием экспортированных сущностей, их следует импортировать в соответствующий файл.
Для начала, давайте разберемся, что TS считает модулем. Спецификация JS определяет, что любой файл без export или await верхнего уровня является скриптом, а не модулем.
Переменные и типы, объявленные в скрипте, являются глобальными (имеют глобальную область видимости), для объединения нескольких файлов на входе в один на выходе следует использовать либо настроку компилятора outFile , либо несколько элементов script в разметке (указанных в правильном порядке).
Если у нас имеется файл, который не содержит import или export , но мы хотим, чтобы этот файл считался модулем, просто добавляем в него такую строку:
Существует 3 вещи, на которые следует обращать внимание при работе с модулями в TS :
- Синтаксис: какой синтаксис я хочу использовать для импорта и экспорта сущностей?
- Разрешение модулей: каковы отношения между названиями модулей (или их путями) и файлами на диске?
- Результат: на что должен быть похож код модуля?
Импорт одного экспортированного элемента
импортируемый элемент также может быть переименован
Виртуальные каталоги с rootDirs
Исходные файлы проекта, находящиеся в разных каталогах, иногда объединяются на этапе компиляции, чтобы сгенерировать единственный выходной каталог. Это можно рассматривать как создание из набора исходных каталогов одного "виртуального" каталога.
Используя 'rootDirs', можно сообщить компилятору о корневых каталогах (roots), составляющих этот "виртуальный" каталог, давая возможность компилятору разрешить команды относительного импорта модулей в пределах этих "виртуальных" каталогов, как если бы они были объединены в один каталог.
Для примера давайте рассмотрим следующую структуру проекта:
В src/views находятся файлы с пользовательским кодом для элементов UI. Файлы в generated/templates содержат код связывания шаблонов пользовательского интерфейса, автоматически сгенерированный генератором шаблонов как часть сборки. На одном из шагов сборки файлы из /src/views и /generated/templates/views будут скопированы в такие же директории в выходной структуре проекта. Представление (view) во время выполнения программы ожидает, что её шаблон находится рядом, и его можно импортировать с помощью относительного пути "./template" .
Чтобы указать компилятору на эту связь, используйте "rootDirs" . "rootDirs" определяет список корневых директорий (roots), чьё содержимое необходимо объединить динамически. Продолжая наш пример, файл tsconfig.json должен выглядеть следующим образом:
Каждый раз, когда компилятор встречает относительный импорт модуля в подкаталоге одного из rootDirs , он пытается найти этот импорт в записях rootDirs .
Внешние модули
В Node.js, большинство задач выполняется с помощью загрузки одного или нескольких модулей. Мы могли бы определить каждый модуль в его собственном файле .d.ts в объявлениями экспорта верхнего уровня, но гораздо удобнее поместить определения всех модулей в одном общем файле .d.ts . Чтобы это сделать, используйте конструкцию, похожую на внешние пространства имён. В ней используется ключевое слово module и заключенное в кавычки имя модуля, которое будет доступно для дальнейшего импорта. Например:
node.d.ts (упрощенный отрывок)
Теперь мы можем указать /// node.d.ts и загрузить модули с помощью import url = require("url"); .
Экспорт объявления
Любое объявление (переменой, функции, класса, псевдонима типа или интерфейса) может быть экспортировано с помощью добавления ключевого слова export .
Validation.ts
ZipCodeValidator.ts
Относительный и неотносительный импорт модулей
Импорт модуля разрешается разными способами в зависимости от того, является ли ссылка относительной или неотносительной.
Относительный импорт начинается с / , ./ или ../ . Примеры:
- import Entry from "./components/Entry";
- import < DefaultHeaders >from "../constants/http";
- import "/mod";
Любой другой импорт считается неотносительным. Примеры:
- import * as $ from "jQuery";
- import < Component >from "angular2/core";
Относительный импорт разрешается относительно импортируемого файла и не может разрешиться объявлением внешнего модуля. Относительный импорт лучше использовать для своих модулей, которые во время выполнения программы гарантированно находятся в указанном месте.
Неотносительный импорт может быть разрешен относительно baseUrl или с помощью сопоставления путей, которое будет описано ниже. Он также может разрешаться объявлениями внешних модулей. Используйте неотносительные пути при импорте любых внешних зависимостей.
Base URL
baseUrl часто используется в приложениях, использующих загрузчик модулей AMD, где модули динамически "разворачиваются" в одном каталоге. Исходные файлы этих модулей могут находиться в разных местах, но скрипт сборки поместит их все в одну директорию.
Установка baseUrl сообщает компилятору о том, где искать модули. Все команды импорта модулей с неотносительными именами считаются относительными baseUrl .
Значение baseUrl определяется как одно из:
- значение аргумента командной строки baseUrl (если передан относительный путь, он рассчитывается относительно текущей директории)
- значение свойства baseUrl в 'tsconfig.json' (если передан относительный путь, он рассчитывается на основе расположения 'tsconfig.json')
Заметьте, что установка baseUrl не влияет на команды относительного импорта модулей, так как они всегда разрешаются относительно импортирующих файлов.
См. дополнительную информацию о baseUrl в документации по RequireJS and SystemJS.
Взаимодействие CommonJS с ES-модулями
Между CommonJS и ES-модулями имеется несовпадение, поскольку ES-модули поддерживают "дефолтный" экспорт только объектов, но не функций. Для преодоления данного несовпадения в TS используется флаг компиляции esModuleInterop .
Разрешение модулей — это процесс определения файла, указанного в качестве ссылки в строке из инструкции import или require .
TS предоставляет две стратегии разрешения модулей: классическую и Node . Классическая стратегия является стратегией по умолчанию (когда флаг module имеет значение, отличное от commonjs ) и включается для обеспечения обратной совместимости. Стратегия Node имитирует работу Node.js в режиме CommonJS с дополнительными проверками для .ts и .d.ts .
Существует большое количество флагов, связанных с разрешением модулей: moduleResolution , baseUrl , paths , rootDirs и др.
Имеется две настройки, которые влияют на результирующий JS-код :
-
— определяет версию JS , в которую компилируется TS-код — определяет, какой код используется для взаимодействия модулей между собой
То, какую цель (target) использовать, зависит от того, в какой среде будет выполняться код (какие возможности поддерживаются этой средой). Это может включать в себя поддержку старых браузеров, более низкую версию Node.js или специфические ограничения, накладываемые такими средами выполнения, как, например, Electron .
Коммуникация между модулями происходит через загрузчик модулей (module loader), определяемый в настройке module . Во время выполнения загрузчик отвечает за локализацию и установку всех зависимостей модуля перед его выполнением.
Ниже приведено несколько примеров использования синтаксиса ES-модулей с разными настройками module :
Модули UMD
Некоторые библиотеки созданы таким образом, чтобы использоваться со многими загрузчиками модулей или без загрузчиков вообще (глобальные переменные). Их называют UMD или изоморфными (Isomorphic) модулями. Такие библиотеки можно подключить и с помощью импорта, и как глобальную переменную. Например:
math-lib.d.ts
Эту библиотеку можно подключить внутри модуля с помощью импорта:
Также эту библиотеку можно подключить как глобальную переменную, но это возможно сделать только внутри скрипта. (Скрипт — это файл без команд импорта и экспорта.)
Синтаксис
Основной экспорт в файле определяется с помощью export default :
Затем данная функция импортируется следующим образом:
В дополнению к экспорту по умолчанию, из файла может экспортироваться несколько переменных и функций с помощью export (без default ):
Указанные сущности импортируются так:
CommonJS
TS имеет собственный модульный формат, который называется namespaces . Данный синтаксис имеет множество полезных возможностей по созданию сложных файлов определений и по-прежнему активно используется в DefinitelyTyped . Несмотря на то, что namespaces не признаны устаревшими (deprecated), большая часть его возможностей нашла воплощение в ES-модулях , поэтому настоятельно рекомендуется использовать официальный синтаксис.
VPS серверы от Маклауд быстрые и безопасные.
Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!
Начиная с ECMAScript 2015, в JavaScript появилась концепция модулей. TypeScript использует ту же концепцию.
Модули выполняются не в глобальной, а в своей собственной области видимости. Это означает, что переменные, функции, классы и т.д., объявленные в модуле, не видны вне модуля, за исключением тех случаев, когда они явно экспортированы с использованием одной из форм export . Также, чтобы использовать переменную, функцию, класс, интерфейс, и т.д., экспортированные из другого модуля, необходимо импортировать их с помощью одной из форм import .
Модули декларативны, и отношения между модулями определяются в терминах импорта и экспорта на файловом уровне.
Модули импортируют друг друга, используя загрузчик модулей, который во время выполнения кода находит и выполняет все зависимости модуля перед его выполнением. В JavaScript широко используются такие загрузчики, как CommonJS для Node.js и require.js для веб-приложений.
В TypeScript, как и в ECMAScript 2015, любой файл, содержащий import или export верхнего уровня, считается модулем.
Отслеживание разрешения модулей
Как упоминалось ранее, компилятор имеет возможность выходить за пределы текущей директории при разрешении модулей. Такое поведение может затруднять диагностику причин, по которым модуль не был разрешен или был разрешен неверно. Чтобы получить представление о том, как проходит процесс разрешения модулей, можно воспользоваться ключом компилятора --traceResolution .
Предположим, что у нас есть простое приложение, использующее модуль typescript . В app.ts находится инструкция импорта import * as ts from "typescript" .
Вызываем компилятор с опцией --traceResolution
Что искать в трассировке
======== Resolving module 'typescript' from 'src/app.ts'. ========
Module resolution kind is not specified, using 'NodeJs'.
- Загрузку объявлений типов (typings) из npm-пакетов
'package.json' has 'typings' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'.
======== Module name 'typescript' was successfully resolved to 'node_modules/typescript/lib/typescript.d.ts'. ========
Объявления модулей с использованием знаков подстановки
Некоторые загрузчики модулей, такие как SystemJS и AMD, позволяют импортировать контент, отличный от JavaScript. В таких случаях обычно используется префикс или суффикс, чтобы обозначить специальную семантику загрузки. Объявления модулей с использованием знаков подстановки могут использоваться для организации загрузок такого типа.
Теперь можно импортировать элементы, совпадающие с "*!text" или "json!*" .
Если вы экспортируете несколько объектов, поместите их на верхний уровень
MyThings.ts
Соответственно при импорте:
Импорт всего модуля в одну переменную, и её использование для доступа к экспортированным элементам модуля
Экспортируйте настолько близко к верхнему уровню, насколько это возможно
Чем меньше будет у пользователей модуля проблем с использованием экспортированных элементов, тем лучше. Добавление уровней вложенности делает модуль более громоздким, поэтому необходимо тщательно обдумывать его структуру.
Экспорт из модуля пространства имён как раз является примером добавления лишнего уровня вложенности. Несмотря на то, что пространства имён бывают полезны, они добавляют в модули ещё один уровень абстракции, что очень скоро может привести к проблемам для пользователей, и обычно не нужно.
Статические методы экспортируемых классов вызывают сходные проблемы, так как класс сам по себе добавляет уровень вложенности. Допустимо пойти на это в том случае, если вы точно знаете, что делаете, и введение дополнительного уровня вложенности добавит выразительности и ясно отразит назначение модуля. В противном случае рекомендуется использовать вспомогательные функции (helper function).
Используйте шаблон импорта пространства имен в случае импорта большого количества элементов
MyLargeModule.ts
Consumer.ts
Импорт модуля ради «побочных эффектов»
Несмотря на то, что так делать не рекомендуется, некоторые модули устанавливают некое глобальное состояние, которое может быть использовано другими модулями. У этих модулей может не быть экспортируемых элементов, или пользователю эти элементы не нужны. Для импорта таких модулей используйте команду:
Каждый модуль может содержать экспорт по умолчанию. Экспорт по умолчанию выделяется ключевым словом default , и в модуле может быть только одна такая инструкция. Для импорта экспорта по умолчанию используется отдельная форма оператора import .
Экспорт по умолчанию может оказаться очень полезным. Например, такая библиотека, как Jquery, может по умолчанию экспортировать jQuery или $ , что мы, вероятно, также импортируем под именем $ или jQuery .
JQuery.d.ts
App.ts
Классы и определения функций могут быть сразу обозначены в качестве экспортируемых по умолчанию. Такие классы и функции могут быть объявлены без указания имён.
ZipCodeValidator.ts
Test.ts
StaticZipCodeValidator.ts
Test.ts
Экспортируемым по умолчанию элементом можно быть обычное значение:
OneTwoThree.ts
Log.ts
У CommonJS и AMD существует концепция объекта exports , который содержит весь экспорт модуля.
Они также поддерживают замену объекта exports единичным пользовательским объектом. Экспорт по умолчанию призван заменить этот функционал. Оба подхода, однако, несовместимы. TypeScript поддерживает конструкцию export = , которую можно использовать для моделирования привычной схемы работы CommonJS и AMD.
Конструкция export = определяет единичный объект, экспортируемый из модуля. Это может быть класс, интерфейс, пространство имён, функция или перечисление.
Для импорта модуля, экспортированного с помощью export = , должна быть использована специфичная для TypeScript конструкция import let = require("module") .
ZipCodeValidator.ts
Test.ts
В зависимости от цели модуля, указанной во время компиляции, компилятор сгенерирует соответствующий код для Node.js (CommonJS), require.js (AMD), (UMD), SystemJS или собственных модулей ECMAScript 2015 (ES6). Для получения более подробной информации по поводу того, что делают вызовы define , require и register в сгенерированном коде, смотрите документацию по каждому отдельному модулю.
В этом простом примере показано, как имена, используемые во время импорта и экспорта, транслируются в код загрузки модуля.
SimpleModule.ts
AMD / RequireJS SimpleModule.js
CommonJS / Node SimpleModule.js
UMD SimpleModule.js
Система SimpleModule.js
Собственные модули ECMAScript 2015 SimpleModule.js
Ниже мы упростили реализацию валидатора из предыдущего примера, сведя его к экспорту единичного именованного экспорта из каждого модуля.
Для успешной компиляции необходимо указать цель модуля в командной строке. Для Node.js, используется --module commonjs ; для require.js — --module amd . Например:
В результате компиляции каждый модуль становится отдельным .js -файлом. Так же как и со ссылочными тегами, компилятор по операторам import найдёт и скомпилирует зависимые файлы.
Validation.ts
LettersOnlyValidator.ts
ZipCodeValidator.ts
Test.ts
В некоторых случаях может потребоваться загрузить модуль только при определённых условиях. В TypeScript возможно использовать приведённый ниже пример, чтобы применить данную или иную продвинутую технику загрузки модулей. Этот приём может использоваться для непосредственного вызова загрузчиков модулей без потери типобезопасности.
Компилятор для каждого модуля определяет, используется ли он в генерируемом JavaScript. Если идентификатор модуля есть только в описаниях типа и никогда в выражениях, тогда для этого модуля не будет сгенерирован вызов require . Такое пропускание неиспользуемых ссылок улучшает производительность, а также позволяет организовать опциональную загрузку модулей.
Основная идея примера заключается в том, что команда import > даёт доступ к типам, раскрываемым данным модулем. Как показано в блоке if ниже, загрузчик модуля вызывается динамически (с помощью require ). Таким образом применяется оптимизация пропуска неиспользуемых ссылок, что приводит к загрузке модуля только тогда, когда он нужен. Чтобы данный приём сработал, необходимо, чтобы идентификатор, определённый с помощью import , использовался только в описании типа (т.е. никогда в таком месте кода, которое попадёт в итоговый JavaScript).
Для поддержки типобезопасности используется ключевое слово typeof . Ключевое слово typeof , при использовании его в описании типа, создаёт тип значения (тип модуля в данном случае).
Динамическая загрузка модулей в Node.js
Пример: динамическая загрузка модулей в require.js
Пример: Динамическая загрузка модулей в System.js
Чтобы описать библиотеку, написанную не на TypeScript, необходимо объявить API, предоставляемый этой библиотекой.
Мы называем объявления, которые не определяют реализации, "внешними" (ambient). Обычно они задаются в файлах .d.ts . Если вы знакомы с C/C++, можете воспринимать их как заголовочные файлы .h . Давайте посмотрим на несколько примеров.
Синтаксис ES-модулей с поведением CommonJS
Синтаксис ES-модулей в TS напрямую согласуется с CommonJS и require из AMD . Импорт с помощью ES-модулей в большинстве случаев представляет собой тоже самое, что require в указанных окружениях, он позволяет обеспечить полное совпадение TS-файла с результатом CommonJS :
CommonJS — это формат, используемый большинством npm-пакетов . Даже если вы используете только синтаксис ES-модулей , понимание того, как работает CommonJS , поможет вам в отладке приложений.
Стратегии разрешения модулей
Существует две стратегии разрешения модулей: Node и Classic. Для указания выбранной стратегии вы можете использовать флаг --moduleResolution . По умолчанию используется стратегия Node.
Использование --noResolve
Обычно компилятор пытается разрешить все инструкции импорта модулей до начала процесса компиляции. Каждый раз, когда он успешно разрешает import в файл, этот файл добавляется в набор файлов, который компилятор обработает позже.
Опция --noResolve говорит компилятору не "добавлять" в компиляцию файлы, которые не были явно указаны в командной строке. Компилятор всё равно попытается разрешить модули в файлы, но не включит в сборку те, которые не были явно указаны.
app.ts
Компиляция app.ts с использованием --noResolve приведет к следующим результатам:
- moduleA будет успешно найдено, поскольку было передано в командной строке.
- Поиск moduleB завершится ошибкой, так как его не было в командной строке.
Почему модуль, находящийся в списке исключенных, тем не менее используется компилятором?
tsconfig.json преобразует каталог в “проект”. Без указания пунктов “exclude” или “files” в сборку включаются все файлы в каталоге, содержащем tsconfig.json , а также в его подкаталогах. Для исключения некоторых файлов, используйте “exclude” . Используйте “files” , если удобнее явно указать все файлы, вместо того чтобы давать возможность компилятору искать их самостоятельно.
Здесь мы говорили об автоматическом включении с tsconfig.json . Согласно обсуждавшемуся выше, это правило не охватывает разрешение модулей. Если компилятор определит, что какой-либо файл является целевым для импорта модуля, этот файл будет включен в сборку независимо от того, был ли он исключен на предыдущих шагах.
Таким образом, чтобы исключить файл из сборки, необходимо исключить его самого и все файлы, в которых есть команды import или /// , ссылающиеся на него.
Индикаторы опасности
Ниже приведен список тревожных признаков, касающихся структурирования модулей. Лишний раз убедитесь, что вы не пытаетесь создавать пространства имен для ваших внешних модулей, если любое из следующих утверждений относится к вашей ситуации:
> Для лучшего понимания данного раздела документации необходимо знание основ работы с модулями. См. modules для получения более подробной информации.
Разрешение модулей (Module resolution) — это используемый компилятором процесс выяснения того, на что ссылается команда импорта. Рассмотрим инструкцию следующего вида: import < a >from "moduleA" . Чтобы проверить корректность использования a , компилятор должен точно знать, что представляет из себя этот элемент, для чего необходимо проверить соответствующее определение - moduleA .
На данном этапе компилятор должен узнать, какова форма moduleA . Пока всё кажется просто, но moduleA может быть определён в одном из файлов .ts / .tsx или .d.ts .
Сначала компилятор попытается найти файл, представляющий импортируемый модуль. Для этого он должен выбрать одну из двух стратегий: Classic или Node. С помощью этих стратегий компилятор определяет, где искать moduleA .
Если найти файл не удалось, и имя модуля не относительное (как в случае "moduleA" ), тогда компилятор попытается найти объявление внешнего модуля (ambient module declaration). Неотносительный импорт (non-relative imports) описан далее.
В итоге, если компилятор не смог разрешить модуль, он выведет ошибку вида error TS2307: Cannot find module 'moduleA'.
Сокращенная запись объявления внешних модулей
Если вы не хотите тратить время на написание объявлений до начала использования нового модуля, можно воспользоваться сокращенным объявлением.
declarations.d.ts
Все импортируемые элементы такого модуля будут иметь тип any .
Ре-экспорт с целью расширения функционала
Зачастую бывает необходимо расширить функциональность модуля. В JavaScript наиболее распространён метод дополнения исходного объекта расширениями (extensions), аналогично тому, как работает JQuery. Как было упомянуто ранее, модули не сливаются подобно объектам глобальных пространств имён. Рекомендуется не изменять исходный объект, а экспортировать новый элемент, предоставляющий новую функциональность.
Давайте рассмотрим реализацию простого калькулятора, созданную в виде модуля Calculator.ts . Из модуля также экспортируется вспомогательная функция, предназначенная для тестирования функциональности калькулятора путём передачи списка входных строк и записи результата.
Calculator.ts
Новый модуль ProgrammerCalculator экспортирует такой же API, что и исходный модуль Calculator , но при этом не изменяет в нём ни одного объекта. Ниже приведён тест класса 'ProgrammerCalculator':
TestProgrammerCalculator.ts
import < Calculator, test >from "./ProgrammerCalculator"; let c = new Calculator(2); test(c, "001+010 -13">Не используйте в модулях пространства имён
Когда программисты только начинают использовать организацию кода с помощью модулей, они часто размещают экспортируемые элементы в пространствах имён, создавая таким образом дополнительные уровни вложенности. Но у модулей есть своя собственная область видимости, и извне видны только экспортированные элементы. Поэтому пространства имён не способны принести ощутимую пользу при работе с модулями.
Пространства имён являются важным инструментом для предотвращения конфликтов имён. Например, у вас могут быть My.Application.Customer.AddForm и My.Application.Order.AddForm — два типа с один именем, но разными пространствами имен. А с модулями такой проблемы не будет. Нет серьёзных оснований для создания двух объектов с одинаковым именем внутри модуля. С точки зрения пользователя, он может выбрать любое имя для импортируемого модуля, поэтому случайные конфликты имен невозможны.
Более подробная информация о пространствах имен и модулях Namespaces and Modules.
Classic
Эта стратегия раньше была принята в TypeScript's по умолчанию. Но теперь она сохранена лишь для обратной совместимости.
Относительный импорт будет разрешен относительно импортируемого файла. Таким образом, import < b >from "./moduleB" в исходном файле /root/src/folder/A.ts приведет к поиску следующих файлов:
- /root/src/folder/moduleB.ts
- /root/src/folder/moduleB.d.ts
При неотносительном импорте модулей, компилятор, пытаясь найти подходящий файл определений, пройдет по дереву каталогов, начиная с директории, содержащей импортирующий файл.
Неотносительный импорт из moduleB , такой как import < b >from "moduleB" , расположенный в файле с исходным кодом /root/src/folder/A.ts , приведет к поиску "moduleB" в следующих местах:
- /root/src/folder/moduleB.ts
- /root/src/folder/moduleB.d.ts
- /root/src/moduleB.ts
- /root/src/moduleB.d.ts
- /root/moduleB.ts
- /root/moduleB.d.ts
- /moduleB.ts
- /moduleB.d.ts
Эта стратегия копирует поведение работающего динамически механизма разрешения модулей Node.js. См. полное описание алгоритма разрешения Node.js в документации по модулям Node.js.
Как Node.js разрешает модули
Чтобы понять, каким путем пойдет компилятор TS, важно немного разобраться в модулях Node.js. Импорт в Node.js выполняется с помощью вызова функции require . Node.js будет действовать по-разному в зависимости от того, указан ли в require относительный или неотносительный путь.
Использование относительных путей обычно не вызывает затруднений. Для примера давайте рассмотрим файл /root/src/moduleA.js , в котором есть следующая инструкция иморта var x = require("./moduleB"); Node.js разрешает этот импорт в таком порядке:
Как файл с именем /root/src/moduleB.js , если он существует.
Как каталог /root/src/moduleB , если в нём есть файл package.json , который определяет модуль "main" . В нашем примере, если Node.js нашла файл /root/src/moduleB/package.json , содержащий < "main": "lib/mainModule.js" >, тогда она сошлётся на /root/src/moduleB/lib/mainModule.js .
Если каталог /root/src/moduleB содержит файл с именем index.js , по умолчанию считается, что он является main-модулем данного каталога.
Вы можете найти дополнительную информацию в документации по Node.js: file modules и folder modules.
Однако, разрешение неотносительных имен модулей выполняется иным способом. Node будет искать ваши модули в специальном каталоге, называемом node_modules . Он может быть на том же уровне иерархии каталогов, что и текущий файл, или выше. Node пойдет вверх по цепочке каталогов, просматривая каждый node_modules , пока не найдет модуль, который вы пытались загрузить.
Продолжая рассматривать наш пример, предположим, что в /root/src/moduleA.js использовался неотносительный путь, и команда импорта выглядела следующим образом: var x = require("moduleB"); . Node попытается разрешить moduleB в один из следующих путей и остановится на первом подходящем.
- /root/src/node_modules/moduleB.js
- /root/src/node_modules/moduleB/package.json (если он определяет свойство "main" )
- /root/src/node_modules/moduleB/index.js
Заметьте, что Node.js поднялась на один уровень на шагах (4) и (7).
Вы можете найти дополнительную информацию в документации по Node.js в разделе загрузка модулей из node_modules .
Как TypeScript разрешает модули
TypeScript копирует стратегию динамического разрешения модулей в Node.js с целью поиска файлов с определениями модулей во время компиляции. С этой целью TypeScript применяет логику Node.js для работы с собственными типами файлов .ts , .tsx и .d.ts . TypeScript также использует поле "typings" в package.json , чтобы отразить назначение "main" - указание компилятору, где находится "основной" файл определений ("main" definition file).
Например, команда импорта import < b >from "./moduleB" в /root/src/moduleA.ts приведёт к поиску "./moduleB" в следующих местах:
- /root/src/moduleB.ts
- /root/src/moduleB.tsx
- /root/src/moduleB.d.ts
- /root/src/moduleB/package.json (если он определяет свойство "typings" )
- /root/src/moduleB/index.ts
- /root/src/moduleB/index.tsx
- /root/src/moduleB/index.d.ts
Напомним, что Node.js пыталась найти файл moduleB.js , затем подходящий package.json , а после index.js .
Неотносительный импорт будет следовать логике разрешения модулей Node.js, сначала пытаясь найти файл, а затем подходящую директорию. Таким образом, import < b >from "moduleB" в файле с исходным кодом /src/moduleA.ts приведёт к поиску в следующих местах:
- /root/src/node_modules/moduleB.ts
- /root/src/node_modules/moduleB.tsx
- /root/src/node_modules/moduleB.d.ts
- /root/src/node_modules/moduleB/package.json (если он определяет свойство "typings" )
- /root/src/node_modules/moduleB/index.ts
- /root/src/node_modules/moduleB/index.tsx
- /root/src/node_modules/moduleB/index.d.ts
Не пугайтесь большого количества пунктов - TypeScript также перешёл на уровень вверх лишь дважды: на шагах (8) и (15). На самом деле это не сложнее того, что делает Node.js.
Специфичный для TS синтаксис модулей
Типы могут экспортироваться и импортироваться с помощью такого же синтаксиса, что и значения в JS :
TS расширяет синтаксис import с помощью import type , что позволяет импортировать только типы.
Такой импорт сообщает транспиляторам, вроде Babel , swc или esbuild , какой импорт может быть безопасно удален.
Экспортное определение (Export statement)
Экспортные определения удобно применять в том случае, когда экспортируемые элементы необходимо переименовать. Тогда вышеприведённый пример можно переписать следующим образом:
Если вы экспортируете только один class или одну function , используйте export default
Аналогично "экспорту максимально близко к верхнему уровню", использование экспорта по умолчанию (default export) облегчает жизнь пользователям вашего модуля. Если основной задачей модуля является размещение и экспортирование одного специфического элемента, то необходимо всерьез рассмотреть использование экспорта по умолчанию. Такой подход делает и саму процедуру импорта, и использование импортированных элементов немного проще. Например:
MyClass.ts
MyFunc.ts
Consumer.ts
Такой подход оптимален для пользователей модуля. Они могут дать вашему типу наиболее удобное для них наименование ( t в данном случае) и будут избавлены от лишнего обращения «через точку» для поиска ваших объектов.
Экспорт
Идентификаторы экпортируются посредством установки свойства exports глобальной переменной module :
Затем эти файлы импортируются с помощью инструкции require :
В данном случае импорт можно упростить с помощью деструктуризации:
Явно определяйте импортированные имена
Consumer.ts
ES2020
Дополнительный синтаксис импорта
Название импортируемой сущности можно менять с помощью import < old as new >:
Разные способы импорта можно смешивать:
Все экспортированные объекты при импорте можно поместить в одно пространство имен с помощью * as name :
Файлы можно импортировать без указания переменных:
В данном случае import ничего не делает. Тем не менее, весь код из maths.ts вычисляется (оценивается), что может привести к запуску побочных эффектов, влияющих на другие объекты.
Дополнительные флаги системы разрешения модулей
Исходная структура проекта не всегда соответствует тому, что получается на выходе. Обычно для достижения результата нужно несколько шагов. Это и компиляция файлов .ts в .js , и копирование зависимостей из различных источников в один выходной файл. В итоге получается, что модули в процессе выполнения могут иметь имена, отличные от имен исходных файлов с их определениями. Пути модулей в итоговом выводе также могут отличаться от соответствующих первоначальных путей на этапе компиляции.
В TypeScript есть набор дополнительных флагов, с помощью которых можно сообщить компилятору о тех трансформациях, которые должны произойти с исходниками, чтобы сгенерировать итоговый вывод.
Важно отметить, что компилятор не будет выполнять эти трансформации. Он лишь использует полученную информацию, чтобы выполнить процесс разрешения импорта модуля в его файл определения.
Ре-экспорт
Модули часто расширяют другие модули. При этом они сами предоставляют доступ к части функций исходных модулей. Ре-экспорт не выполняет локального импорта и не создаёт локальную переменную.
ParseIntBasedZipCodeValidator.ts
При использовании модуля в качестве обёртки над одним или несколькими другими модулями, есть возможность ре-экспортировать сразу все их операторы экспорта с помощью конструкции export * from "module" .
AllValidators.ts
Импортировать практически так же просто, как и экспортировать. Импорт экспортированного объявления выполняется с помощью одной из форм import , приведённых ниже:
Сопоставление путей
Иногда модули не находятся прямо под baseUrl. Например, команда импорта модуля "jquery" во время выполнения будет преобразована к "node_modules\jquery\dist\jquery.slim.min.js" . Загрузчики используют конфигурацию сопоставления путей, чтобы динамически установить соответствие имен модулей и соответствующих файлов, см. документацию по RequireJs и SystemJS.
Компилятор TypeScript поддерживает объявление подобных сопоставлений в свойстве "paths" файла tsconfig.json . Вот пример того, как можно указать свойство "paths" для jquery .
Свойство "paths" позволяет использовать более сложные методы сопоставления, включая множественные резервные пути. Давайте рассмотрим конфигурацию, в которой в одном расположении доступны лишь некоторые модули, оставшиеся же находятся в другом. При сборке все эти модули будут помещены в одно место. Схема проекта может выглядеть следующим образом:
Соответствующий tsconfig.json будет выглядеть следующим образом:
Таким образом мы сообщаем компилятору, что для каждого модуля, инструкция импорта которого соответствует шаблону "*" (то есть любые значения), он должен выполнить поиск в двух местах:
- "*" : означающее то же самое имя без изменений, поэтому сопоставляем =>\
- "generated\*" означающее имя модуля с добавленным префиксом "generated", поэтому сопоставляем =>\generated\
Следуя этой логике, компилятор попытается разрешить указанные инструкции импорта следующим образом:
- import 'folder1/file2'
- есть соответствие шаблону '*', под который подпадает имя модуля целиком;
- пробуем первую замену по списку: '*' -> folder1/file2 ;
- результатом замены является относительное имя, соединяем его с baseUrl -> projectRoot/folder1/file2.ts ;
- Файл существует. Готово.
- import 'folder2/file3'
- есть соответствие шаблону '*', под который подпадает имя модуля целиком;
- пробуем первую замену по списку: '*' -> folder2/file3
- результатом замены является относительное имя, соединяем его с baseUrl -> projectRoot/folder2/file3.ts .
- Файл не существует, переходим к следующей замене
- вторая замена 'generated/*' -> generated/folder2/file3
- результатом замены является относительное имя, соединяем его с baseUrl -> projectRoot/generated/folder2/file3.ts .
- Файл существует. Готово.
Читайте также: