Элемент import можно использовать только в файлах typescript ts 8002
На самом деле привычный всем механизм импорта\экспорта таит в себе множество курьезов способных нарушить ожидаемый ход выполнения программы. Помимо детального рассмотрения каждого из них, текущая глава также расскажет о способах их разрешения.
Предыстория возникновения import type и export type
Представьте сценарий, по которому существуют два модуля, включающих экспорт класса. Один из этих классов использует другой в аннотации типа своего единственного параметра конструктора, что требует от модуля в котором он реализован, импорта другого модуля. Подвох заключается в том, что несмотря на использование класса в качестве типа, модуль в котором он определен вместе с его содержимом, все равно будет скомпилирован в конечную сборку.
При использовании допустимых JavaScript конструкций исключительно в качестве типа, было бы разумно ожидать, что конечная сборка не будет обременена модулями, в которых они определены. Кроме того, конструкции, присущие только TypeScript, не попадают в конечную сборку, в отличие от модулей, в которых они определенны. Если в нашем примере поменять тип конструкции SecondLevel с класса на интерфейс, то модуль ./FirstLevel.js все равно будет содержать импорт модуля ./SecondLevel.js , содержащего экспорт пустого объекта export <>; . Не лишним будут обратить внимание, что в случае с интерфейсом, определяющий его модуль мог содержать и другие конструкции. И если бы среди этих конструкций оказались допустимые с точки зрения JavaScript, то они, на основании изложенного ранее, попали бы в конечную сборку. Даже если бы вообще не использовались.
Это поведение привело к тому, что в TypeScript появился механизм импорта и экспорта только типа. Этот механизм позволяет устранить рассмотренные случаи. Тем не менее, имеется несколько нюансов, которые будут подробно изложены далее.
import type и export type - форма объявления
Форма уточняющего импорта и экспорта только типа включает в себя ключевое слово type , идущее следом за ключевым словом import либо export .
Ключевое слово type можно размещать в выражениях импорта, экспорта, а также ре-экспорта.
Пусть у нас будет в проекте файл devices.ts :
Чтобы классы, интерфейсы, функции были видны извне, они определяются с ключевым словом export .
Но мы могли бы и по другому экспортировать все сущности:
Импорт
Чтобы задействовать модуль в приложении, его надо импортировать с помощью оператора import . Например, импортируем класс Phone и функцию Call из выше определенного модуля devices.ts:
После слова import определяется набор импортируемых типов - класов, интерфейсов, функций, объектов. А после слова from указывается путь к модулю. В данном случае модуль располагается в файле devices.js, который находится в той же папке, поэтому в начале пути ставится точка и далее указывается название файла без расширения. Если бы модуль располагался бы в папке lib, находящеся в текущем каталоге, то название папки также бы включалось в путь к модулю: "./lib/devices".
Псевдонимы
При экспорте и импорте для компонента модуля можно указать псевдоним с помощью оператора as :
Например, установка псевдонима для компонента при импорте:
Так, в данном случае для функции call() установлен псевдоним makeCall() , и далее мы обращаемся к функции call() через ее псевдоним makeCall() .
Также псевдоним можно установить при экспорте компонента:
Затем компонент импортируется через его псевдоним:
Импорт всего модуля
Можно импортировать сразу весь модуль:
В данном случае модуль импортируется через псевдоним "dev". И, используя этот псевдоним, мы можем обращаться к расположенным в этом модуле типам.
Экспорт по умолчанию
Параметры экспорта по умолчанию позволяют определить тип, который будет импортироваться из модуля по умолчанию. К примеру, добавим новый модуль smartwatch.ts :
Ключевое слово default позволяет установить класс SmartWatch в качестве типа по умолчанию. И затем мы можем импортировать его следующим образом:
При этом мы можем установить для экспортируемого по умолчанию компонента другое имя:
Начиная с ECMAScript 2015, в JavaScript появилась концепция модулей. TypeScript использует ту же концепцию.
Модули выполняются не в глобальной, а в своей собственной области видимости. Это означает, что переменные, функции, классы и т.д., объявленные в модуле, не видны вне модуля, за исключением тех случаев, когда они явно экспортированы с использованием одной из форм export . Также, чтобы использовать переменную, функцию, класс, интерфейс, и т.д., экспортированные из другого модуля, необходимо импортировать их с помощью одной из форм import .
Модули декларативны, и отношения между модулями определяются в терминах импорта и экспорта на файловом уровне.
Модули импортируют друг друга, используя загрузчик модулей, который во время выполнения кода находит и выполняет все зависимости модуля перед его выполнением. В JavaScript широко используются такие загрузчики, как CommonJS для Node.js и require.js для веб-приложений.
В TypeScript, как и в ECMAScript 2015, любой файл, содержащий import или export верхнего уровня, считается модулем.
Экспорт объявления
Любое объявление (переменой, функции, класса, псевдонима типа или интерфейса) может быть экспортировано с помощью добавления ключевого слова export .
Validation.ts
ZipCodeValidator.ts
Экспортное определение (Export statement)
Экспортные определения удобно применять в том случае, когда экспортируемые элементы необходимо переименовать. Тогда вышеприведённый пример можно переписать следующим образом:
Ре-экспорт
Модули часто расширяют другие модули. При этом они сами предоставляют доступ к части функций исходных модулей. Ре-экспорт не выполняет локального импорта и не создаёт локальную переменную.
ParseIntBasedZipCodeValidator.ts
При использовании модуля в качестве обёртки над одним или несколькими другими модулями, есть возможность ре-экспортировать сразу все их операторы экспорта с помощью конструкции export * from "module" .
AllValidators.ts
Импортировать практически так же просто, как и экспортировать. Импорт экспортированного объявления выполняется с помощью одной из форм import , приведённых ниже:
Импорт одного экспортированного элемента
импортируемый элемент также может быть переименован
Импорт всего модуля в одну переменную, и её использование для доступа к экспортированным элементам модуля
Импорт модуля ради «побочных эффектов»
Несмотря на то, что так делать не рекомендуется, некоторые модули устанавливают некое глобальное состояние, которое может быть использовано другими модулями. У этих модулей может не быть экспортируемых элементов, или пользователю эти элементы не нужны. Для импорта таких модулей используйте команду:
Каждый модуль может содержать экспорт по умолчанию. Экспорт по умолчанию выделяется ключевым словом 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 . Давайте посмотрим на несколько примеров.
Внешние модули
В Node.js, большинство задач выполняется с помощью загрузки одного или нескольких модулей. Мы могли бы определить каждый модуль в его собственном файле .d.ts в объявлениями экспорта верхнего уровня, но гораздо удобнее поместить определения всех модулей в одном общем файле .d.ts . Чтобы это сделать, используйте конструкцию, похожую на внешние пространства имён. В ней используется ключевое слово module и заключенное в кавычки имя модуля, которое будет доступно для дальнейшего импорта. Например:
node.d.ts (упрощенный отрывок)
Теперь мы можем указать /// <reference> node.d.ts и загрузить модули с помощью import url = require("url"); .
Сокращенная запись объявления внешних модулей
Если вы не хотите тратить время на написание объявлений до начала использования нового модуля, можно воспользоваться сокращенным объявлением.
declarations.d.ts
Все импортируемые элементы такого модуля будут иметь тип any .
Объявления модулей с использованием знаков подстановки
Некоторые загрузчики модулей, такие как SystemJS и AMD, позволяют импортировать контент, отличный от JavaScript. В таких случаях обычно используется префикс или суффикс, чтобы обозначить специальную семантику загрузки. Объявления модулей с использованием знаков подстановки могут использоваться для организации загрузок такого типа.
Теперь можно импортировать элементы, совпадающие с "*!text" или "json!*" .
Модули UMD
Некоторые библиотеки созданы таким образом, чтобы использоваться со многими загрузчиками модулей или без загрузчиков вообще (глобальные переменные). Их называют UMD или изоморфными (Isomorphic) модулями. Такие библиотеки можно подключить и с помощью импорта, и как глобальную переменную. Например:
math-lib.d.ts
Эту библиотеку можно подключить внутри модуля с помощью импорта:
Также эту библиотеку можно подключить как глобальную переменную, но это возможно сделать только внутри скрипта. (Скрипт — это файл без команд импорта и экспорта.)
Экспортируйте настолько близко к верхнему уровню, насколько это возможно
Чем меньше будет у пользователей модуля проблем с использованием экспортированных элементов, тем лучше. Добавление уровней вложенности делает модуль более громоздким, поэтому необходимо тщательно обдумывать его структуру.
Экспорт из модуля пространства имён как раз является примером добавления лишнего уровня вложенности. Несмотря на то, что пространства имён бывают полезны, они добавляют в модули ещё один уровень абстракции, что очень скоро может привести к проблемам для пользователей, и обычно не нужно.
Статические методы экспортируемых классов вызывают сходные проблемы, так как класс сам по себе добавляет уровень вложенности. Допустимо пойти на это в том случае, если вы точно знаете, что делаете, и введение дополнительного уровня вложенности добавит выразительности и ясно отразит назначение модуля. В противном случае рекомендуется использовать вспомогательные функции (helper function).
Если вы экспортируете только один class или одну function , используйте export default
Аналогично "экспорту максимально близко к верхнему уровню", использование экспорта по умолчанию (default export) облегчает жизнь пользователям вашего модуля. Если основной задачей модуля является размещение и экспортирование одного специфического элемента, то необходимо всерьез рассмотреть использование экспорта по умолчанию. Такой подход делает и саму процедуру импорта, и использование импортированных элементов немного проще. Например:
MyClass.ts
MyFunc.ts
Consumer.ts
Такой подход оптимален для пользователей модуля. Они могут дать вашему типу наиболее удобное для них наименование ( t в данном случае) и будут избавлены от лишнего обращения «через точку» для поиска ваших объектов.
Если вы экспортируете несколько объектов, поместите их на верхний уровень
MyThings.ts
Соответственно при импорте:
Явно определяйте импортированные имена
Consumer.ts
Используйте шаблон импорта пространства имен в случае импорта большого количества элементов
MyLargeModule.ts
Consumer.ts
Ре-экспорт с целью расширения функционала
Зачастую бывает необходимо расширить функциональность модуля. В 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.
Индикаторы опасности
Ниже приведен список тревожных признаков, касающихся структурирования модулей. Лишний раз убедитесь, что вы не пытаетесь создавать пространства имен для ваших внешних модулей, если любое из следующих утверждений относится к вашей ситуации:
TypeScript (TS) позволяет использовать аннотации типов в коде JavaScript. TS даже может проверять код при сборке, благодаря чему вы увидите ошибки до того, как они попадут в продакшен. Вы избавитесь от undefined is not a function навсегда.
TypeScript по умолчанию требует некоторых изменений при настройке окружения. Вам придётся переименовать файлы JavaScript в .ts, .tsx, а также использовать компиляторы tsc или Babel.
Синтаксис TypeScript
Часто людям не нравится работать с TypeScript из-за необходимости использовать новый для них синтаксис. Если вам знакома эта ситуация, статья как раз для вас.
Синтаксис TypeScript позволяет использовать аннотации типов инлайн. Но сначала поговорим об альтернативах.
Документируем JavaScript
Синтаксис JSDoc
TypeScript можно документировать с помощью JSDoc. Это стандартный синтаксис для документирования JavaScript. JSDoc используется для создания документации, но TypeScript понимает аннотации типов, созданные с помощью этого инструмента.
Это значит, что у вас есть возможности использовать преимущество TypeScript, в частности, проверку типов, без необходимости конвертировать весь код.
Почему JSDoc
Применение JSDoc — полезная практика, даже если вы не используете TypeScript. Фактически это стандарт документирования JavaScript, и его поддерживают разные инструменты и редакторы.
Если вы уже применяете JSDoc, вам будет удобно использовать проверку типов в коде, аналогичную той, которая применяется в TypeScript. Для этого нужно уделить время настройке TypeScript.
Установка TypeScript
Как установить TypeScript
Чтобы установить в проект TypeScript, используйте такую команду:
Как включить проверку типов JSDoc
Теперь нужно настроить TypeScript, чтобы он проверял код в файлах JavaScript. По умолчанию он проверяет только файлы с расширением .ts . Настройки TypeScript надо указывать в файле tsconfig.json . Обратите внимание на опцию noEmit . Мы используем её, так как планируем применять TypeScript только для проверки типов.
Настраиваем TypeScript
В начале файлов .js , в которых вам нужна проверка типов, добавьте комментарий:
Запустите проверку типов. Это можно сделать с помощью команды:
Рекомендуется использовать проверку типов также в инструментах непрерывной интеграции (CI).
Дальше поговорим о документировании кода с помощью JSDoc.
Базовые аннотации
Аннотации параметров функций
Для аннотации параметров функций используйте @param . Его нужно указать в комментариях JSDoc, которые начинаются с двух идущих подряд астериксов.
Документирование кода
JSDoc — инструмент для документирования. Кроме добавления аннотаций типов, вы можете документировать функции.
Потренируемся в документировании.
Документирование параметров
Опциональные типы
Чтобы показать опциональность типа, добавьте после него знак равенства. В примере ниже number= — то же самое, что и number | null | undefined . Такой синтаксис можно использовать только в типах JSDoc.
Документируем опции
Вы можете документировать свойства параметров, например, options.count или options.separator . Эту возможность можно использовать для документирования props в функциональных компонентах React.
Утверждения типов (Type Assertions)
Переменные
Используйте @type , когда пишете инлайн определение для аргументов функций. Это обычно избыточно для констант, так как TypeScript чётко работает с типами. Подход полезен при работе с изменяемыми данными, например, с переменными.
Параметры функций
@type можно использовать для определения типов аргументов функций инлайн. Это особенно удобно при работе с анонимными функциями.
Далее поговорим о выносе определений типов в отдельные файлы.
Импорт определений типов
Импортируем типы
Сложные и переиспользуемые типы лучше определять во внешних файлах. Они имеют расширение .d.ts . Обратите внимание, это должны быть именно файлы TypeScript. Импортировать определения из файлов JavaScript невозможно.
Типы можно импортировать с помощью представленного ниже синтаксиса. Определения должны определяться во внешних файлах с расширением .d.ts , как сказано выше.
Определяем типы во внешних файлах
Ниже представлен синтаксис определения типов во внешних файлах TypeScript. Ещё раз обратите внимание, импортировать определения типов из файлов JavaScript невозможно.
Теперь разберёмся, можно ли определять типы в JavaScript-файлах.
Определение типов в файлах JavaScript
Типы объектов
Для определения типов объектов используйте @typedef . Предпочтительно делать это во внешних файлах с расширением .d.ts . Но вы можете использовать представленный ниже синтаксис и в файлах JavaScript.
Объединение типов
Используйте объединение типов ( | ) для определения двух или более возможных вариантов. Для простоты используйте @typedef .
Как насчёт React?
Определение типов в React
Функциональные компоненты
Функциональные компоненты представляют собой функции. Поэтому вы можете документировать их способами, о которых говорилось выше в разделе о документировании функций. В следующем примере показано документирование с помощью типов объектов.
Подробности о функциональных компонентах можно узнать в курсе по React, который входит в профессию «Фронтенд JavaScript».
Компоненты на классах
Используйте @extends для определения типов props и state . Также для решения этой задачи можно использовать @typedef инлайн или с импортом.
Расширенные возможности
Синтаксис JSDoc не такой выразительный, как TypeScript, но эти инструменты всё-таки похожи. Ниже перечислены некоторые дополнительные возможности TS, доступные в JSDoc.
- Темплейты — @templates .
- Возврат значений — @returns .
- Условия типов — @returns .
- Функциональные типы — @callback .
- Перечисления — @enum .
Ещё больше возможностей найдёте в официальной документации.
Резюме
Ниже представлены перечисленные в статье способы работы с аннотациями типов.
Проверка типов JavaScript
Документирование функций
Импорт определений типов (позволяет определять типы во внешних файлах)
Опциональные типы
Анонимные функции
Документирование свойств параметров объектов
Адаптированный перевод статьи Type annotations in JavaScript files by Rico Sta. Cruz. Мнение администрации Хекслета может не совпадать с мнением автора оригинальной публикации.
Читайте также: