Typescript расширение файла tsx

Обновлено: 05.07.2024

React - это библиотека для создания пользовательских интерфейсов, от компании Facebook. В основе библиотеки продвинутого рендера React лежит компонентный подход, для улучшения которого, стандартный синтаксис JavaScript был расширен XML-подобным синтаксисом. В связи с этим было создано новое расширение .jsx.

Из-за высокой популярности React, создателями TypeScript было принято решение создать расширение .tsx. Но одного расширения недостаточно чтобы благополучно компилировать .tsx. Так как реакт можно использовать для создания как вэб, так и мобильных приложений, компилятору с помощью флага --jsx нужно указать конкретную принадлежность к “react” , либо “react-native” . По умолчанию выставлено значение “preserve” .

За исключением типизации, написание шаблонов .tsx ничем не отличается от .jsx. Но так как при переходе с .jsx на .tsx возникает лишь один вопрос, как правильно писать те или иные виды компонентов в типизированном стиле, то данная глава будет отличаться от остальных и будет построена таким образом, чтобы общую информацию изучить по ходу детального рассмотрения каждого вида компонентов в отдельности. Кроме того, если раньше примеры кода были придуманы таким образом, чтобы помимо информативности, быть ещё и компактными, то в случаи с компонентами React, ничего поделать нельзя. Чтобы хоть как-то сгладить это, компоненты будут показываться небольшими частями, а уже после будет описываться важные-неочевидные моменты. Помимо этого, компоненты не будут иметь какой-то осмысленный характер. Если попытаться смоделировать, что-то, что можно продемонстрировать, то кода было бы в разы больше и сделало его менее понятным.

React - производные от Component

Первым делом стоит обратить внимание, на объявление, после четырех интерфейсов, идентификаторы которых начинаются с идентификатора создаваемого компонента *DefaultProps , *Props , *State , *Snapshot .

По названиям не сложно догадаться, что в первом интерфейсе, предполагается описывать свойства имеющие значение по умолчанию, а во втором обязательные свойства создаваемого компонента. Второй интерфейс предназначен для описания состояния компонента. Ну а третий интерфейс описывает снимок, который возвращает метод getSnapshotBeforeUpdate() .

К тому же модуль, к которому принадлежит рассматриваемый код, экспортирует интерфейс *Props . У этого есть веская причина, которая будет рассматриваться далее в теме посвященной созданию HOC.

Следующее, на что стоит обратить внимание, это создание псевдонимов типа для всех объявленных ранее интерфейсов.

Сделано это по причине того, что идентификаторы типов, для которых создаются псевдонимы, настолько сложные и большие, что использование их в многочисленных аннотациях, в теле компонента, сделает его трудно читаемым. Кроме того, тип Readonly , которые защищают объектные типы от случайных изменений, часто дополняется типом Partial .

Далее, в теле класса компонента, происходит объявление defaultProps и state . Нужно обратить внимание, что эти поля объявлены лишь в демонстрационных целях, так из-за отсутствия у них членов, в них нет потребности. Кроме того,при их объявлении также используется модификатор readonly , чтобы защитить сами поля от случайного изменения.

Затем, в теле класса компонента, происходит объявление полей, которые нужны для получение ссылок на нативные dom элементы ( ref ).

Два остальных поля предназначены для получения ссылки на нативный dom элемент с помощью функции обратного вызова (callback). Для этого сначала объявляется поле, которому в дальнейшем будет присвоено значение, принадлежащие к типу html элемента, в данном случаи это элемент span HTMLSpanElement . На следующем шаге объявляется поле, в качестве значения которого выступает стрелочная функция, в теле которой происходит присваивание dom элемента, ожидаемого в качестве единственного параметра, полю, объявленному на предыдущем шаге. Стоит обратить внимание, что описание типа функции обратного вызова, происходит до объявления компонента и в целях повышения семантики кода, для него создается псевдоним типа TextRefCallback , который и используется в аннотации поля. Для предотвращения случайного изменения, поле, в качестве значения которому присвоена ссылка на функцию обратного вызова, объявлен с модификатором readonly .

Следующим по очереди идет конструктор, в объявлении которого нет ничего необычно. Вслед за ним, объявляется поле closeButton_clickHandler , которому в качестве значения присваивается стрелочная функция, выступающая в роли слушателя события (event handler).

И прежде, чем перейти к рассмотрению кода в её теле, обязательно стоит пролить свет на один очень важный момент, связанный с аннотированием типов. Как было сказано в начале этой главы, все примеры являются классическими, с точки зрения типизации, но несмотря на это, всем, кто придерживается подобного подхода в повседневной разработке, рекомендуется делать отступления от правил, особенно в случаях с функциями передаваемых в качестве значения. В данном случае, подобное аннотирование, является явным перебором.

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

Если же, для операций в теле слушателя, требуется объект event , то обойтись без аннотации уже не получится. Вывод типов не сможет вывести тип, а значит параметр event будет принадлежать к типу Any и следовательно автодополнение будет отсутствовать. Но в случаи нативных слушателей событий, существует аж два способа явной аннотации.

Первая из них заключается в аннотировании параметра event, без аннотирования поля, которому присваивается слушатель событий. Из-за того, что этот код нельзя трактовать двусмысленно, не страдает его читаемость. Но кроме того, вывод типов сможет вывести тип для поля, если информацию о нем потребуется получить с помощью подсказок предоставляемых всеми современными idea.

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

Какой способ выбрать зависит лишь от конвенций по стилю кода установленного в команде и от специфики некоторых ситуации.

Далее стоит обратить внимание на код в теле слушателя событий, а именно операции асинхронного изменения состояния при помощи функции, также называемой “функциональным состоянием”. Именно этот способ был выбран по той причине, что он отлично дополняет сказанное относительно отступления от правил касающихся классического подхода при аннотировании языковых конструкций.

В том случае, если бы передаваемая в метод setState функция, по каким-либо причинам, нуждалась в явной аннотации возвращаемого типа, и при этом из нее возвращалось лишь часть состояния, то пришлось бы описывать создавать описания типа с нуля. Другими словами, указать тип State не получилось бы, так как тип State не совместим с типом обладающими лишь некоторыми его признаками (глава Типизация - Совместимость объектов). Либо пришлось описывать члены типа State , из-за которых возникает несоответствие, необязательными (необязательные свойства рассматриваются в главе Операторы - Optional, Not-Null, Not-Undefined, Definite Assignment Assertion). Ситуация не очень распространенная, но о ней нужно знать.

Осталось рассмотреть лишь метод render , у которого все внимание будет приковано к возвращаемому типу.

На самом деле с методом render связан один нюанс, который обязательно нужно знать, а кроме того надеятся, что создатели деклараций для React , исправят положение. И дело вот в чем. Предположим, что разработчику потребовалось создать компонент лист.

И в какой-то момент, разработчик решает воспользоваться силой типизации и поставить точку в давнем вопросе, а именно запретить добавлять в ul , элементы отличные от li . Казалось бы, что может быть проще? Всего-то навсего, нужно для компонента List ограничить тип children типом компонента ListItem .

Эти два факта сводят на нет саму идею аннотирования возвращаемого типа из метода render . Но возможно именно вам и предстоит поменять положение дел и тем самым снискать нескончаемую благодарность реакт сообщества.

React - производные от PureComponent

React - Functional Component

Поскольку в React функциональные компоненты, которые чаще называют Stateless Components, являются обычной функцией, то объявлять их можно двумя способами, в виде обычной функции (Function Declaration) и в виде функционального выражения (Function Expression), декларация которого также разделяется на два варианта, аннотирование самой функции и декларирования ссылки на неё. Так как stateless компоненты очень компактные и в них нет ничего, что не было рассмотрено на предыдущих шагах, здесь будет приведен код всех трех вариантов, но особое внимание будет уделено последнему варианту.

Как уже было сказано, первый способ, аннотирование Function Declaration ничем не отличается от аннотирования обычных функций.

Второй способ, аннотирование функции, выступающей в качестве значения, также не отличается от аннотирования обычной функции.

Все варианты одинаковые и выбор зависит от личных предпочтений. Хотя я склоняюсь к третьему варианту.

React - Обобщенные компоненты (Generics Component)

В TypeScript существует возможность объявлять пользовательские компоненты обобщенными. Это делает работу с ними более удобной, а кроме того, повышает их переиспользование. Чтобы избавить читателя от пересказа того, что подробно было рассказано в главе Типы - Обобщения (Generics), опустим теорию, которая относится к обобщенным типам и сосредоточимся на закреплении их использования. Для этого сначала будет смоделирована проблема, а затем предоставлено её решение при помощью обобщенных типов. Но прежде, рассмотрим сам синтаксис обобщенных компонентов.

В случае компонентов расширяющих классы Component или PureComponent , нет ничего особенного, на что стоит обратить внимание.

Нет ничего особенного и в объявлении функциональной декларации (Function Declaration).

Но относительно функциональных компонентов, в роли которых выступают стрелочные функции (arrow function), без курьезов не обошлось. Дело в том, что на данный момент, TypeScript не поддерживает обобщенные стрелочные функции, в файлах, с расширением .tsx.

Кроме того, объявление обобщенного функционального компонента в виде функционального выражения тоже накладывает некоторые ограничения. В большинстве случаев, использование параметров типа предполагается использовать в обобщенном интерфейсе описывающим свойства ( props ) компонент. А это в свою очередь делает бессмысленным аннотацию идентификатора ссылающегося на функцию.

После того, как обобщенный тип был объявлен, его можно начать использовать в .jsx шаблонах.

На этом, беглое рассмотрение объявления и создания обобщенных компонентов закончено, можно переходить к более практическим примерам.

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

В качестве простого примера, представим, что существует используемый во множестве частей программы, такой компонент, как Select . Каждый раз,когда пользователь выбирает тот или иной Option, компонент Select посылает вместе с событием id выбранного Option . При этом, чтобы воспользоваться этим компонентом, необходимо, по данным, построить дерево состоящее из компонентов Select и Option . То есть появляется потребность в использовании циклов. Кроме того, очень часто, после получения id выбранного Option , требуются операции над данными, с которыми он ассоциирован. Это означает, что каждый раз, когда вызывается слушатель событий, нужно перебирать массив с данными, для поиска в нем элемента с нужным id .

Представьте сколько действий нужно проделать. А если в приложении, будет создаваться множество компонентов Select , то эти действия придется повторить для каждого отдельного случая. Налицо нарушения принципа DRY (“Don't repeat yourself” или по русски “Не повторяйся”).

Кроме того, бывают случаи, когда массив с данными, по которому строятся компоненты, собирается на основе других данных и сразу же передается в тот компонент, в котором с ним происходит дальнейшая работа. В случаи, когда с этими данными нужно работать после того, как по ним отрендерятся визуальные компоненты, их требуется сохранить в локальное состояние. При повторении данного сценария, это снова приведет к повторению кода и кроме того код программы будет усложнен операциями по сохранению и поддержке данных в актуальном состоянии. Это приводит к нарушению ещё одного принципа известного как принцип KISS (“Keep it simple, stupid”).

Чтобы устранить эти проблемы, разработчику придется создать новый слой абстракции (новый компонент), который инкапсулирует всю работу над компонентом Select .

Как видно из примера, у разработчика получилось решить проблему с хранением данных и повторение кода, но тем не менее появилась другая. Данный компонент нельзя назвать универсальным, так как жестко зависит от типа IContactInfo , что причисляет код к “неправильному”. Этот “неправильный” код, специально был включен в примеры, чтобы ещё раз показать начинающим разработчикам, как делать нельзя. Нельзя завязывать код на специфических типах. Если потребуется отобразить данные не принадлежащие к типу IContactInfo , придется создать новый компонент и тем самым снова нарушить принцип DRY.

Как минимум тип данных, которые ожидает универсальный компонент, должен иметь собственную декларацию и описывать только минимально требующиеся для успешного выполнения характеристики.

В этот раз получилось избавится от всех описанных проблем (повторения кода, усложнения программы, отсутствие универсальности), но появилась новая. Теперь данные, которые компонент передает в качестве аргументов при вызове слушателя событий, ограничены типом данных ISelectData , в то время, как ожидается IContactForSelectWithDataInfo , который не получится получить без явного приведения типов (механизм приведения типов рассматривается в главе Типизация - Утверждение типов). Простыми словами, отсутствие возможности работать с компонентом не прибегая к механизмам преобразования типов, делает его не полностью универсальным. Решить это, можно было бы с помощью более общего типа, коим является тип any . Но в таком случае снизится типобезопасность программы, к тому же разработчик лишится такого замечательного механизма, как автодополнение.

В типизированных языках, проблемы с универсальностью, решается с помощью механизма создания обобщенных типов. К тому же TypeScript, поддерживает синтаксис параметризированных компонентов, что будет продемонстрировано в последнем примере.

Оставшейся код ничем не отличается от предыдущего, за исключением введения обобщений, поэтому комментариев по нему не будет. Но стоит обратить внимание, на параметры типов, как у типа Props , так и функционального компонента. Для того, чтобы не возникло ситуации, при которой данные не будут соответствовать минимально требующемуся, для успешной работы компонента, типу данных, параметр типа, объявленный в типе Props расширяет тип данных ISelectData .И так как тип Props, имеет параметр типа ограниченный типом данных ISelectData , тип, выступающий в качестве аргумента типа Props , должен также быть совместимым с типом ISelectData . Другими словами, параметр типа функционального компонента также должен расширять тип данных ISelectData или же совместимый с ним.

React - HOC (Higher-Order Components)

Очень часто, при разработке React приложений, разработчикам приходится создавать конструкцию, известную в react сообществе, как HOC (Higher-Order Components).

HOC, это функция, которая принимает компонент и возвращает новый компонент. Другими словами, hoc, это функция, ожидающая в качестве параметров компонент (назовем его входным), который оборачивается в другой, объявленный в теле функции, компонент, который выступает в роли возвращаемого из функции значения (назовем его выходным). Слово “оборачивание” применимое относительно компонентов, означает, что один компонент, отрисовывает (рендерит) другой компонент, со всеми вытекающими из этого процесса (проксирование). За счет того, что входной компонент оборачивается в выходной, достигается расширение его и\или общего функционала. Кроме того, это позволяет устанавливать входному компоненту, как зависимости, так и данные, полученные из внешних сервисов.

Если объяснения, что такое hoc и в каких случаях в нем появляется необходимость, выходит за рамки данной книги, то с примерами, иллюстрирующими самые распространенные случаи, для общей картины, ознакомится все же стоит.

Но прежде чем приступить к краткому рассмотрению примеров, будет полезно более подробно ознакомиться с сигнатурой hoc-функции

Как уже было сказано, входной компонент, оборачивается, в объявленный в теле функции, выходной компонент, который может быть как классовым так и функциональным компонентом. И прежде чем кратко ознакомится с каждым из случаев, будет полезно, отдельно рассмотреть сигнатуру hoc-функции на распространенном в практике примере, когда свойства ( props ) выходного компонента, лишь дополняются свойствами входного компонента.

Первым делом, при необходимости, декларируются тип-интерфейс, описывающий специфические для выходного компонента свойства ( props ). Далее, объявляется параметр типа InputProps , который представляет специфические для входного компонента свойства и который расширяет тип данных OutputProps . В данном простом примере, в теле hoc, оперировать требуется таким типом, которому полностью соответствует композиция типов входных и входных свойств. Поэтому для удобства и сокращения кода, тип входных параметров расширяет тип выходных параметров. Но стоит заметить, что бывают случаи, один из которых будет рассмотрен далее, при котором подобное недопустимо.

Теперь перейдем к рассмотрению кода, описывающего hoc целиком. Но так как его сигнатура уже была подробно изучена, а в его теле не происходит ничего, что не было бы рассмотрено в этой главе, нет необходимости в каких-либо комментариях.

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

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

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

Чтобы начать использовать JSX, необходимо сделать следующее:

  1. Назначить вашим файлам расширение .tsx
  2. Включить опцию jsx

TypeScript имеет два JSX режима: preserve и react . Эти режимы влияют только на стадию генерации - проверка типов не изменяется. Режим preserve сохраняет JSX в выходном коде, который далее передаётся на следующий шаг трансформации (e.g. Babel). Выходной код получит расширение .jsx . Режим react сгенерирует React.createElement , где уже не нужно трансформировать JSX перед применением, и код на выходе получит расширение .js .

Вы можете указать режим либо с помощью флага командной строки --jsx , либо в соответствующей опции в вашем файле tsconfig.json.

Замечание: Идентификатор React жёстко прописан в коде, поэтому необходимо сделать React доступным с заглавной буквы R.

Вспомним, как записывается декларирование типов:

Здесь мы декларируем, что переменная bar будет иметь тип foo . Так как TypeScript также использует угловые скобки для декларирования типов, синтаксис JSX's становится труднее обработать. В результате TypeScript запрещает использование угловых скобок при декларировании типов в файлах .tsx .

Оператор as доступен как в .ts так и в .tsx файлах и ведёт себя точно также, как и другой оператор декларирования.

Чтобы понять проверку типов в JSX, необходимо уяснить разницу между внутренними элементами и элементами, основанными на значении. В JSX-выражении <expr /> , expr может означать как внутренний элемент окружения (например, div или span в окружении DOM), так и созданный вами пользовательский элемент. Это важно по следующим причинам:

  1. В React внутренние элементы генерируются в виде строк ( React.createElement("div") ), а пользовательские компоненты нет ( React.createElement(MyComponent) ).
  2. Типы атрибутов, передаваемых в JSX-элемент, получаются разными способами. Внутренние элементы должны быть известны по умолчанию, тогда как компоненты, скорее всего, будут создавать свои собственные наборы атрибутов.

TypeScript использует то же соглашение, что и React, чтобы различать два вышеупомянутых случая. Внутренние элементы всегда начинаются с маленькой буквы, а элементы, основанные на значении, начинаются с заглавной.

Внутренние элементы

Система находит внутренние элементы с помощью специального интерфейса JSX.IntrinsicElements . По умолчанию, если этот интерфейс не определён, тип всех внутренних элементов не будет проверен. Однако если интерфейс определён, система будет искать имя внутреннего элемента как свойство интерфейса JSX.IntrinsicElements . Например:

В примере выше <foo /> отработает нормально, но <bar /> приведёт к ошибке, так как он не был определён в JSX.IntrinsicElements .

Замечание: Вы также можете определить универсальный строковый индексатор JSX.IntrinsicElements :

Элементы-значения

Система ищет элементы-значения по идентификаторам в пределах области видимости.

Есть возможность ограничить тип элемента-значения. Но для этого необходимо ввести два новых термина: тип класса элемента (element class type) и тип экземпляра элемента (element instance type).

В выражении <Expr /> типом класса элемента является тип Expr . Таким образом, в вышеприведённом примере, если MyComponent принадлежит классу ES6, типом класса будет именно этот класс. Если MyComponent является фабричной функцией, тип класса будет этой функцией.

Как только тип класса установлен, тип экземпляра определяется объединением возвращаемых типов сигнатуры вызова типа класса и сигнатуры конструктора. Опять же, в случае класса ES6, типом экземпляра будет тип экземпляра этого класса, а в случае фабричной функции это будет тип возвращаемого функцией значения.

Также вызывает интерес тип элемента экземпляра, поскольку должна быть возможность назначить его JSX.ElementClass , в противном случае возникнет ошибка. По умолчанию JSX.ElementClass является <> , но он может быть дополнен, чтобы ограничить использование JSX только теми типами, которые соответствуют правильному интерфейсу.

Проверка типа атрибута

Для того, чтобы проверить типы атрибутов, сначала необходимо определить тип атрибутов элемента (element attributes type). Эта процедура немного отличается для внутренних элементов и элементов-значений.

Для внутренних элементов это тип свойства в JSX.IntrinsicElements

Для элементов-значений вопрос немного усложняется. Тип атрибутов определяется типом типом экземпляра элемента (element instance type), который был установлен ранее. Какое свойство использовать, определяется с помощью JSX.ElementAttributesProperty . Оно должно быть объявлено единственным свойством, имя которого будет использоваться далее.

Тип атрибута элемента используется для проверки типов атрибутов в JSX. Поддерживаются опциональные и обязательные свойства.

Замечание: Если имя атрибута не является корректным JS-идентификатором (как атрибут data-* ), это не будет считаться ошибкой, если не будет находиться в типе атрибутов элемента.

Также можно использовать оператор расширения:

По умолчанию результирующим типом выражения JSX является тип any . Вы можете изменить тип путём определения интерфейса JSX.Element . Однако невозможно получить информацию о типах элемента, атрибутов или потомков JSX из интерфейса. Фактически это чёрный ящик.

JSX позволяет вставлять выражения между тегами, заключая их в фигурные скобки ( < >).

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

Для работы JSX с React необходимо использовать React typings. Эти типы определяют пространство имён JSX для корректного использования с React.

Инструменты для статической типизации, такие как Flow или TypeScript, позволяют отлавливать большую часть ошибок ещё до исполнения кода. Кроме того, они существенно улучшают процессы разработки, добавляя автодополнение и другие возможности. Для приложений с большой кодовой базой мы рекомендуем использовать Flow или TypeScript вместо PropTypes .

Flow — это библиотека для статической типизации JavaScript, разработанная в Facebook и часто применяемая в связке с React. Flow расширяет возможности JavaScript, добавляя аннотации типов для переменных, функций и React-компонентов. Ознакомиться с основами Flow можно на странице официальной документации.

Чтобы начать пользоваться возможностями Flow необходимо:

  • добавить Flow в ваш проект как зависимость.
  • убедиться, что аннотации Flow удаляются из кода при его компиляции.
  • добавить несколько аннотаций типов и запустить Flow для их проверки;

Рассмотрим подробнее каждый из этих шагов.

Добавление Flow в проект

Убедитесь, что вы находитесь в директории проекта, после чего запустите одну из следующих команд:

Если вы используете Yarn:

Если вы используете npm:

Эти команды добавят последнюю версию Flow в ваш проект.

Далее нужно добавить flow в секцию "scripts" файла package.json :

Теперь можно запустить скрипт, прописав в терминале:

Эти команды создадут файл с исходной конфигурацией Flow, который обязательно нужно закоммитить.

Удаление аннотаций Flow из скомпилированного кода

Flow дополняет JavaScript собственным синтаксисом для указания типов, который не поддерживается браузерами. Для того, чтобы код работал, нужно убедиться в том, что аннотации Flow корректно удаляются из скомпилированного JavaScript.

Для этого есть несколько способов — выбирайте в зависимости от того, какими инструментами для сборки проекта вы пользуетесь.

Create React App

Если для изначальной конфигурации проекта вы выбрали Create React App, вам ничего не нужно делать! Проект уже настроен должным образом и аннотации Flow должны удаляться при сборке проекта.

Примечание:

Дальнейшие инструкции рассчитаны на тех, кто не использует Create React App, т. к. там уже есть все необходимые настройки для работы с Flow.

Если для своего проекта вы самостоятельно настраивали Babel, нужно установить специальный пресет для работы с Flow:

Затем добавьте установленный пресет flow в свою конфигурацию Babel. Например так, если вы используете конфигурационный файл .babelrc :

Этот пресет позволит использовать Flow в вашем коде.

Примечание:

Для работы с Flow не требуется отдельно устанавливать пресет react — Flow уже понимает JSX-синтаксис. Тем не менее, часто используют оба пресета одновременно.

Другие инструменты сборки

Для удаления аннотаций Flow существует отдельная библиотека: flow-remove-types. Она может пригодиться, если вы пользуетесь другими инструментами для сборки проекта.

Если всё было сделано правильно, можно попробовать запустить процесс Flow:

Добавление аннотаций типов

По умолчанию Flow проверяет только файлы, содержащие специальную аннотацию (обычно её указывают в самом начале файла):

Попробуйте добавить эту аннотацию в некоторые файлы вашего проекта, а затем запустить скрипт yarn flow или npm run flow и посмотреть, найдёт ли Flow какие-нибудь ошибки.

Кроме того, есть возможность заставить Flow проверять вообще все файлы. Если вы переводите на Flow проект, в котором уже есть наработки кода, может возникнуть множество конфликтов, а вот для старта с нуля такая опция может стать неплохим выбором.

Всё должно работать! Советуем изучить Flow подробнее, ознакомившись со следующими ресурсами:

TypeScript — это язык программирования, разработанный в Microsoft. TypeScript является надмножеством JavaScript, имеет статическую систему типов и собственный компилятор. Статическая типизация позволяет отлавливать ошибки и баги во время компиляции, ещё до запуска приложения. Подробнее узнать о совместном использовании TypeScript и React можно здесь.

Чтобы использовать TypeScript, нужно:

  • добавить TypeScript в проект как зависимость.
  • настроить компилятор.
  • использовать правильные расширения файлов.
  • установить файлы объявлений для используемых библиотек;

Остановимся подробнее на каждом из этих моментов.

Использование TypeScript вместе с Create React App

Create React App поддерживает TypeScript по умолчанию.

Чтобы создать новый проект с поддержкой TypeScript, используйте следующую команду:

Можно добавить поддержку TypeScript в уже существующий проект, как показано здесь.

Примечание:

Дальше описывается ручная настройка TypeScript. Если вы используете Create React App, можете пропустить этот раздел.

Добавление TypeScript в проект

Всё начинается с одной единственной команды в терминале:

Ура! Вы установили последнюю версию TypeScript. Теперь в вашем распоряжении новая команда — tsc . Но прежде, чем праздновать, давайте добавим соответствующий скрипт в файл package.json :

Настройка компилятора TypeScript

Сам по себе компилятор бесполезен, пока мы не скажем, что именно ему нужно делать. Для этого есть специальный конфигурационный файл tsconfig.json . Создадим этот файл:

Сгенерированный файл tsconfig.json уже содержит несколько параметров, которые используются компилятором по умолчанию. Кроме того, можно указать множество опциональных параметров. Более детальная информация по каждому параметру находится здесь.

Из всех параметров больше всего сейчас нас интересуют rootDir и outDir . Очевидно, что компилятор берёт исходный TypeScript код, и компилирует его в JavaScript. И нам нужно, чтобы не возникло путаницы между исходными файлами и сгенерированным кодом.

Эту проблему можно решить в два шага:

  • Во-первых, изменим структуру проекта. Все файлы с исходниками переместим в директорию src .
  • Затем, укажем компилятору откуда ему брать исходные файлы и куда сохранять скомпилированный код.

Отлично! Теперь, если мы запустим скрипт сборки проекта, компилятор сохранит готовый JavaScript в директорию build . В TypeScript React Starter уже есть готовый tsconfig.json с неплохим набором параметров для дальнейшей тонкой настройки под себя.

Как правило, скомпилированный JavaScript-бандл не следует хранить в системе контроля версий, так что не забудьте добавить папку build в файл .gitignore .

В React мы почти всегда используем .js в качестве расширений файлов компонентов. В TypeScript лучше разделять файлы на два типа:

.tsx для файлов, содержащих разметку JSX , и .ts для всего остального.

Если всё было сделано правильно, можно попробовать скомпилировать TypeScript:

Если эта команда не вывела ничего в терминале, то процесс компиляции прошёл успешно.

Для анализа ошибок и выдачи всплывающих подсказок компилятор TypeScript использует файлы объявлений. Они содержат в себе всю информацию о типах, которые используются в конкретной библиотеке. В свою очередь это позволяет нам использовать JavaScript-библиотеки в проекте совместно с TypeScript.

Существует два основных способа получения файлов объявлений:

Bundled — библиотека устанавливается вместе с собственным файлом объявлений. Это прекрасный вариант для нас, так как всё, что нам нужно — установить нужный пакет. Чтобы проверить, есть ли у библиотеки файл объявлений, поищите index.d.ts в её исходных файлах. В некоторых библиотеках наличие и расположение этого файла указываются в package.json в секциях typings или types .

DefinitelyTyped — это внушительный репозиторий файлов объявлений. Например, React устанавливается без собственного файла объявления — вместо этого мы устанавливаем его отдельно:

Иногда пакет, который вы хотите использовать, не имеет ни собственного файла объявлений, ни соответствующего файла в репозитории DefinitelyTyped. В этом случае, мы можем объявить собственный локальный файл объявлений. Для этого надо создать файл declarations.d.ts в корне директории, где лежат исходники вашего проекта. Файл объявлений может выглядеть примерно так:

Вот и всё, вы готовы писать код на TypeScript! Чтобы познакомиться с ним поближе, рекомендуем посетить эти ресурсы:

Reason — это не новый язык, а новый синтаксис и набор инструментов для проверенного временем языка OCaml. Reason предоставляет синтаксис, ориентированный на JavaScript-программистов, и использует уже известный всем способ распространения через NPM/Yarn.

Reason был разработан в Facebook и используется в некоторых продуктах этой компании — например, в Messenger. Reason всё ещё считается довольно экспериментальным инструментом, но уже имеет библиотеку привязок для React, поддерживаемую Facebook, а также отзывчивое сообщество.

Kotlin — это язык со статической типизацией, разработанный в JetBrains. Он нацелен на платформы работающие на основе JVM, Android, LLVM и JavaScript.

JetBrains разрабатывает и поддерживает несколько библиотек специально для сообщества React: React bindings совместно с Create React Kotlin App. Последняя позволит вам начать использовать Kotlin вместе с React в одном проекте без необходимости ручной конфигурации.

Декларации это очень важная часть TypeScript благодаря которой магия статической типизации проецируется на динамический JavaScript. Поэтому декларациям будет посвящена вся данная глава, рекомендуемая тем, кто только собирается писать свою первую типизированную библиотеку, которую планируется распространять с помощью npm репозитория.

Что такое декларация (Declaration)

Поскольку при разработке программ на TypeScript используются библиотеки написанные на JavaScript, компилятор tsc, чьей главной задачей является проверка типов, чувствует себя будто у него завязаны глаза. Несмотря на то, что с каждой новой версией вывод типов все лучше и лучше учится разбирать JavaScript, до идеала ещё далеко. Кроме того, разбор JavaScript кода добавляет нагрузку на процессор, драгоценного время которого при разработке современных приложений, порой и так не достаточно.

TypeScript решил эту проблему за счет подключения к проекту заранее сгенерированных им или создаваемых вручную разработчиками деклараций. Декларации размещаются в файлах с расширением .d.ts и состоят только из объявлений типов полностью повторяющих программу до момента компиляции при которой она была лишена всех признаков типизации.

Еще не забыты дни, когда для часто используемых библиотек приходилось писать декларации вручную, при чем они часто содержали ошибки. Кроме этого, декларации не успевали обновляться под постоянно развивающиеся библиотеки. Сейчас такие проблемы кажутся уже нереальными, но несмотря на это, до сих пор приходится прибегать к использованию менеджера деклараций, который был создан в те самые далекие времена.

Установка деклараций с помощью @types

Если декларация распространяется отдельно от библиотеки, то она скорее всего, попадет в огромный репозиторий на github под названием DefinitelyTyped содержащий огромное количество деклараций. Чтобы было проще ориентироваться в этом множестве, помимо сайта "TypeSearch" выступающего в роли поисковика, был создан менеджер деклараций под названием Typed. Но о нем мы говорить не будем поскольку он применяется при работе с TypeScript версии меньше чем v2.0, поэтому речь пойдет о его развитии в образе команды пакетного менеджера npm, а именно @types.

Для того, что бы установить требующуюся декларацию, в терминале необходимо выполнить команду, часть которой состоит из директивы @types , после которой через косую черту / следует имя библиотеки.

Для примера, воспользуемся проектом созданным в теме посвященной настройки рабочего окружения и для демонстрации работы директивы @types установим всем известную библиотеку React.

Первым делом установим саму библиотеку React выполнив в терминале, запущенным из-под директории проекта, следующую команду.

Открыв директорию /node_modules/ можно убедиться, что библиотека React успешно установлена, поэтому сразу же попытаемся импортировать её в файл index.js расположенным в директории src, предварительно изменив его расширение на требуемое для работы с React — .tsx .

Несмотря на установленную на предыдущем шаге библиотеку React, при попытке импортировать её модули возникла ошибка. Возникла она потому, что компилятору TypeScript ничего не известно о библиотеке React, поскольку декларация описывающая типы поставляется отдельно от неё. Чтобы tsc понял, что от него хотят, требуется дополнительно установить декларацию при помощи команды @types пакетного менеджера npm .

Ошибка, возникающая при импорте модулей React исчезла, а если заглянуть в директорию _/node_modules/ , то можно увидеть новую примечательную поддиректорию /@types предназначенную для хранения устанавливаемых с помощью опции @types декларации.

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

Кроме того, нужно установить необходимую для работы с ним декларацию.

Осталось только активировать опцию --jsx в _tsconfig.json и скомпилировать проект, как это было показано ранее.

Подготовка к созданию декларации

Помимо того, что декларацию можно написать руками, её также можно сгенерировать автоматически, при условии, что код написан на TypeScript. Для того, что бы tsc при компиляции генерировал декларации, нужно активировать опцию компилятора --declaration .

Будет не лишним напомнить, что декларацию нужно генерировать только тогда, когда библиотека полностью готова. Другими словами, активировать опцию --declaration нужно в конфигурационном файле production сборки. Кроме того, в декларации нуждается только код, который будет собран в подключаемую библиотеку. Поэтому точкой входа в библиотеку должен быть файл, который содержит только импорты нужных модулей. Но разработка библиотеки невозможна без её запуска, а значит и точки входа в которой будет создан и инициализирован её экземпляр. Поэтому, что бы избежать чувства «что-то пошло не так», необходимо помнить, что при создании библиотеки требующей декларацию, в проекте может быть несколько точек входа. Точкой входа самого компилятора, служит конфигурационный файл который ему был установлен при запуске. Это означает, что если проект находится в директории src , то в декларации путь будет указан как src/libName вместо требуемого lib .

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

Это проблему можно решить, разместив конфигурационный файл в директории исходного кода, в нашем случае это директория src . Кто-то не придаст этому значение, кому-то это может показаться неэстетичным. Поэтому при рассмотрении генерации деклараций с помощью tsc, конфигурационный файл будет лежать непосредственно в директории src . Но при рассмотрении генерации деклараций с помощью сторонних библиотек, будет освещен альтернативный вариант.

Но и это ещё не все. Представьте, что Вы создаете библиотеку React, которая в коде представляется одноимённым классом расположенном в файле React.ts . При этом модуль, который будет представлять вашу библиотеку должен называться react , что в свою очередь обязывает задать имя файлу являющегося точкой входа как `react.js. Ну и, что спросите вы? Если вы ещё не знаете ответ на этот вопрос, то будете удивлены, узнав, что существуют операционные системы, как, например, Windows, которые расценивают пути до файлов React.ts и react.ts идентичными. Простыми словами если в директории присутствует файл с идентификатором Identifier, то ОС просто не позволит создать одноимённый файл, даже если его символы будут отличаться регистром. Именно об этом и будет сказано в ошибке, возникающей когда TypeScript обнаружит одноимённые файлы в одной директории. Кроме того, если ваша операционная система позволяет создавать файлы чьи идентификаторы отличаются только регистром, помните, что разработчик работающий с вами в одной команде не сможет даже установить проект себе на машину, если его операционная система работает по другим правилам.

Поэтому решений у этой проблемы, на самом деле, всего два. Задавать идентификаторы отличающиеся не только регистром. Или же размещать файлы таким образом, что бы их идентификаторы не пересекались в одной директории. Но при этом нужно помнить, что структура модулей также изменится.

И поскольку TypeScript является компилируемым языком, не будет лишним напомнить правила именования директории в которую будет компилироваться результат. В случае разработки приложения, директорию содержащую скомпилированный результат принято называть dest (сокращение от слова destination). При разработке внешней библиотеки или фреймворка, директорию для собранных файлов принято называть dist (сокращение от слова distributive).

Разновидности деклараций

На самом деле это глава должна называться «разновидности библиотек», так как именно о них и пойдет речь. Дело в том, что совсем недавно вершиной хорошего тона считалось объединение всего кода в один файл. Это же правило соблюдалось и при создании библиотек. Но сейчас все кардинально поменялось, и дело вот в чем.

В мире JavaScript существует большое количество библиотек, чей размер по меркам клиентских приложений превышает разумный. При этом отказ от них будет означать, что вам самому придется тратить драгоценное время на реализацию части их функционала. Это побудило создателей сборщиков наделять свои творения механизмом называющимся Tree Shaking.

Tree Shaking — это механизм позволяющий включать в сборку исключительно используемый код. Простыми словами, данный механизм позволяет взять только используемую часть от всей библиотеки. В перспективе это должно быть спасением, но на деле оказалось не совсем так.

Дело в том, что на данный момент Tree Shaking работает только если библиотека разбита на множество модулей. К примеру такие именитые библиотеки, как lodash или rxjs, для каждой отдельной функции создают отдельную точку входа, что при их использовании позволят значительно сократить размер конечного кода. Обозначим подобные библиотеки, как библиотеки с множеством точек входа. Кроме того, существуют библиотеки сопоставимые с монолитом, поскольку при использовании их малой части в конечную сборку они попадают целиком. Обозначим такие библиотеки, как библиотеки с единственной точкой входа.

Декларации и область видимости

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

Читайте также: