Это вторая из серии сообщений в блоге, посвященных преобразованию существующей кодовой базы React, Flatris от @Skidding, в Typescript. В итоге мы получим полностью проверенную кодовую базу. На этот раз мы рассмотрим добавление аннотаций типов к простым компонентам React.

На чем мы остановились в прошлый раз

Теперь мы можем запускать Javascript и Typescript одновременно. Это означает, что мы можем преобразовывать вещи постепенно. Давайте начнем с преобразования компонентов React в первую очередь - их легко преобразовать, поскольку у них уже есть система типов, привязанная через PropTypes. Поскольку у Flatris нет набора тестов, мы не можем автоматически тестировать наши рефакторинги. Однако это всего лишь один экран, поэтому ручное тестирование легко. Что мы будем делать, так это переписывать по одному компоненту за раз, запускать игру, чтобы убедиться, что все работает.

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

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

Компоненты, не нуждающиеся в модификации

Некоторые компоненты идентичны между Javascript и TypeScript. Это компоненты, типы которых либо предоставлены сторонней библиотекой, либо компоненты, не получающие реквизитов.

Простой пример:

За исключением расширения файла, эти компоненты TypeScript полностью идентичны своим JS-аналогам.

В Flatris у нас тоже есть некоторые из этих компонентов. Есть кнопка, созданная с помощью styled-components, она выглядит следующим образом:

Для работы нам понадобятся типы библиотеки.

Некоторые библиотеки поставляются с определениями машинописного текста, и в этом случае нам не нужно выполнять дополнительную работу. В популярных библиотеках, которые не поставляются с определениями типов, они находятся в пространстве имен @types на npm.

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

Компоненты без сохранения состояния с реквизитами

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

Первое, что нам нужно сделать, это переименовать его в tsx.

Мы получим следующую ошибку:

C:/…/flatris-ts/src/components/SquareBlock.tsx
(15,13): Property 'propTypes' does not exist on type '({ color }: { color: any; }) => Element'.

Typescript правильно определяет здесь тип нашей функции, так как в качестве функции что-то принимает цветной объект и возвращает элемент JSX. Однако машинописный текст не без ума от случайного добавления свойств к функциям. Это означает, что, когда мы пытаемся добавить propTypes, машинописный код блокируется и сообщает нам, что у этой функции нет свойства propType - что с этим делать?

Вместо простого определения функции, как мы делали это в обычном Javascript выше, мы хотим добавить аннотацию типа, чтобы убедиться, что Typescript считает его правильным компонентом React - propTypes и всем остальным.

В типах React функциональный компонент называется React.SFC, где SFC - это функциональный компонент без сохранения состояния. Попробуем добавить к компоненту аннотацию такого типа:

Это успешно избавляет от ошибки propTypes - но назревает другой шторм ...

Реквизит для набора текста

Здесь мы получаем новую ошибку:

(11,36): Type '{ children?: ReactNode; }' has no property 'color' and no string index signature.

Здесь TypeScript, кажется, жалуется на то, что мы передаем color в нашем компоненте. Странный. Давайте попробуем взглянуть на типизацию React.SFC, чтобы понять, что происходит. Хотя это не самый сложный пример определения типа (в сложных случаях они могут быть очень запутанными), он все же достаточно сложен, чтобы его стоило рассмотреть поближе.

Мы будем рассматривать аннотации типов по одной и попытаемся понять, что происходит. Это будет немного более глубокое погружение в некоторые типы машинописного текста - подождите.

Первый тип такой:

type SFC<P = {}> = StatelessComponent<P>;

Это тип SFC, который мы используем.

Это так называемый псевдоним типа, который обозначается ключевым словом type. По сути, это просто означает «это сокращение для этого типа». Например,

type StringOrNumber = string | number

Псевдоним типа для объединения строки и числа. Это в точности эквивалентно замене StringOrNumber на string | число каждый раз, когда вы его видите.

Теперь следующая часть шрифта: угловые скобки. Часть внутри угловых скобок ‹› обозначает общий тип.

Общий тип здесь называется P - я полагаю, для реквизита. Это та часть, где мы сообщаем Typescript, какие реквизиты мы готовы принять. Он говорит <P = {}> - это означает; «Если тип не указан, по умолчанию используется пустой объект {}, что означает отсутствие дополнительных свойств»

Подводя итог, вся строка означает:

«Когда вы используете SFC, я буду ссылаться на тип StatelessComponent. Мне нужен общий параметр с реквизитами, но если вы мне его не дадите, я предполагаю, что реквизита нет »

Итак, давайте посмотрим на тип StatelessComponent - мы быстро рассмотрим фактический код, а затем разберем его.

Итак, первая часть интерфейса - самая интересная, давайте посмотрим на нее:

(props: P & { children?: ReactNode }, context?: any): ReactElement<any> | null;

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

Первая часть

(props : P & (children?: ReactNode)

это тип пересечения, обозначаемый &. & означает «объединить эти два типа». Таким образом, эти утверждения означают: «Принять параметр props, который представляет собой комбинацию свойств общего параметра, но может также иметь дополнительный параметр, называемый дочерними элементами».

? после детей обозначают, что это необязательно. Это позволяет нам передавать потомков, когда мы хотим, через JSX. Например.

<Foo><Bar/></Foo>

эквивалентно передаче ReactElement Bar в качестве дочернего элемента Foo.

Теперь вторая часть сигнатуры типа:

context?: any

Это означает, что функция также может принимать второй параметр, но он не типизирован. Он предназначен для доступа к устаревшему API контекста React. Теперь посмотрим на возвращаемый тип:

Этот возвращаемый тип означает, что эта функция либо возвращает компонент React, принимающий какие-либо свойства, либо ничего.

Итак, подведем итоги. Этот:

(props: P & { children?: ReactNode }, context?: any): ReactElement<any> | null;

Означает это:

«StatelessComponent может быть вызван с помощью Props, как указано в универсальном параметре P, и, необязательно, с дочерними элементами. Если он принимает другой аргумент, кроме реквизита, это контекст. Это может быть что угодно. Функция либо вернет ReactElement, либо ничего ».

Ух! Это было долгим способом объяснить наше исходное сообщение об ошибке. Просто чтобы пробудить вашу память, сообщение об ошибке было:

(11,36): Type '{ children?: ReactNode; }' has no property 'color' and no string index signature.

Итак, на что TypeScript жалуется здесь, так это на общий параметр в React.SFC. Поскольку по умолчанию используется пустой объект {}, это означает, что если мы не укажем общую часть - единственное, что он будет принимать, это дочерние объекты, например нет реквизита.

Итак, сообщение об ошибке звучит так: Эй, вы сказали мне, что не получаете никаких реквизитов, но сразу после этого вы продолжаете говорить об этой цветовой переменной - что с этим?

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

Это делает его компилируемым без каких-либо проблем, однако TypeScript теперь позволяет нам передавать любые свойства в SquareBlock. Нам это необязательно нужно - чем больше мы используем тип any, тем меньше значения мы получаем от Typescript.

По типам PropTypes мы можем сказать, что цветовой объект должен быть строкой, поэтому давайте запишем это в интерфейс машинописного текста.

Эти проптипы:

SquareBlock.propTypes = {
 color: PropTypes.string.isRequired
};

соответствуют этому интерфейсу:

interface Props {
   color: string;
}

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

Последний компонент без проптипов выглядит так:

Компонент на основе классов

Возьмем более сложный компонент - Тетримоно. Тетромино представляет собой набор из четырех квадратных блоков. Думаю, это то, что большинство людей назвали бы «кусочком тетриса».

Tetromino - это компонент, основанный на классах, поэтому нам придется действовать немного по-другому. Здесь нам не нужно добавлять какой-либо явный тип. При расширении React.Component - TypeScript автоматически подбирает вводимые данные.

Если мы посмотрим на определение класса React.Component, оно выглядит примерно так:

// Note that this is not the actual typings - the react typings are // reasonably complex, so I've edited them a bit down to communicate // the gist of it
class Component<P={}, S={}> {
    constructor(props: P, context?: any);
    // ...Stuff removed for brevity
}

Обратите внимание на два общих параметра, которые по умолчанию равны пустому объекту. Мы можем написать такой код:

class Tetromino extends React.Component {}

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

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

class Tetromino extends React.Component<any, any> {}

Однако - мы можем лучше. Глядя на Tetromino, мы видим, что он не содержит состояния, поэтому давайте изменим второй параметр на void:

class Tetromino extends React.Component<any, void> {}

Или даже лучше - мы можем просто оставить это без внимания!

class Tetromino extends React.Component<any> {}

Теперь займемся реквизитом! Мы снова перепишем propTypes в интерфейс. Типы propTypes выглядят так:

Итак, у нас есть строка и массив массивов чисел. В машинописном тексте это можно выразить так:

interface Props {
    color : string,
    // Note the double square brackets - that's a twice nested array
    grid: number[][] 
}

И мы добавляем это в React.Component, так что теперь наше новое определение класса:

class Tetromino extends React.Component<Props> {

Теперь - мы еще не закончили. Мы получим еще одну ошибку propTypes:

Property 'propTypes' does not exist on type 'typeof Tetromino'.

Итак, Typescript не без ума от того, что мы объявляем новые свойства для уже существующих классов - лучший способ объявить propTypes как свойство на основе класса - использовать статический блок. Так что вместо:

мы получаем:

Конечно, мы можем просто полностью опустить propTypes. В полном объеме мы получим следующий урок.

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

В следующей части мы рассмотрим использование простого анализа времени выполнения для ввода компонентов, которые мы не можем понять, просто взглянув на propTypes. Мы также рассмотрим набор редукторов и действий. Если у вас есть какие-либо комментарии или вопросы, свяжитесь со мной по адресу @GeeWengel.

Если вы просто хотите получать уведомления, когда выйдет следующая часть - подпишитесь на мою рассылку. Я очень редко пишу электронные письма, так как очень ленив.