React/TypeScript 101

TypeScript — это круто. Это позволяет оставаться на диком западе JavaScript, оставаясь при этом в безопасности под одеялом современной системы типов. Кроме того, это отличный способ постепенно внедрить строгую проверку типов без необходимости переписывать всю кодовую базу или получать докторскую степень по теории типов (я смотрю на вас, Haskell 🦥).

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

Минимальный TypeScript, который вам нужно знать, чтобы научиться печатать в React

Начнем с простого компонента React:

// Hello.tsx
import React from 'react';
const Hello = () => {
    return <div>Hello</div>;
};

Чтобы добавить типы к этому компоненту, нам нужно добавить аннотацию типа к Hello. Это то, что React называет «функциональным компонентом» или сокращенно «FC». React — братан, и он также предоставляет нам свой (TypeScript) тип — React.FC:

import React from 'react';
const Hello: React.FC = () => {
    return <div>Hello</div>;
};

Этот (и другие типы, которые мы добавим ниже) являются частью пакетов @types/react и @types/react-dom, поэтому их необходимо установить.

Идем дальше. Приветствовать приятно, но здороваться, обращаясь к человеку по имени, еще приятнее. Давайте научим наш компонент Hello принимать свойство name:

import React from 'react';
const Hello: React.FC = ({ name }) => {
    return <div>{`Hello, ${name}!`}</div>;
};

О нет, появляется страшная красная волнистая линия.

Сейчас самое время поднять важную тему. TypeScript делает свое дело во время компиляции (когда мы пишем код). Таким образом, даже несмотря на то, что здесь жалуются, когда мы действительно запускаем код («среда выполнения»), он будет работать как обычно. В этом смысле это скорее предупреждение, чем ошибка.

Поэтому, если мы проигнорируем ошибки TypeScript, во время выполнения все может работать, а может и не работать. Но если мы не будем игнорировать их, мы можем быть уверены, что все будет работать во время выполнения (то есть то, что может проверять TypeScript).

Вернемся к волнистому. tsc (компилятор типаSскрипта C) говорит нам, что простой React.FC не принимает никаких реквизитов, но здесь мы пытаясь прочитать name.

Итак, нам нужно сообщить tsc, что это не обычный React.FC, это расширенная версия, которая также принимает реквизит name, и что этот name является строкой.

import React from 'react';
const Hello: React.FC<{ name: string }> = ({ name }) => {
    return <div>{`Hello, ${name}!`}</div>;
};

Это удовлетворяет tsc. Но это не очень читабельно, и чем больше реквизитов мы добавим, тем тупее он будет становиться. Итак, давайте определим тип, описывающий реквизиты, которые принимает наш компонент, а затем используем этот тип для улучшения React.FC.

import React from 'react';
interface HelloProps {
    name: string;
}
const Hello: React.FC<HelloProps> = ({ name }) => {
    return <div>{`Hello, ${name}!`}</div>;
};

Идеальный.

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

Но наш компонент Hello идеален сам по себе — как компонент React из учебника, он делает одну вещь, и делает одну вещь хорошо, и является отличным строительным блоком. Добавление новых функций к этому испортило бы это совершенство.

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

import React from 'react';
interface HelloProps {
    name: string;
}
const Hello: React.FC<HelloProps> = ({ name, children }) => {
    return (
        <div>
            <div>{`Hello, ${name}!`}</div>
            {children}
        </div>
    );
};

О нет, красная волнистая линия снова появляется! На этот раз он жалуется, что

Свойство «дети» не существует для типа «HelloProps».

Справедливо. Чтобы исправить это, мы можем добавить children к HelloProps, хотя для этого нам нужно выяснить, каков тип children.

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

Почему, действительно есть. Он называется PropsWithChildren (ура, красивые описательные имена!).

import React, { PropsWithChildren } from 'react';
interface HelloProps {
    name: string;
}
const Hello: React.FC<PropsWithChildren<HelloProps>> = ({ name, children }) => {
    return (
        <div>
            <div>{`Hello, ${name}!`}</div>
            {children}
        </div>
    );
};

Блестящий.

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

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

import React, { PropsWithChildren } from 'react';
interface HelloProps {
    name: string;
}
const Hello: React.FC<PropsWithChildren<HelloProps>> = ({
    name,
    className,
    children,
}) => {
    return (
        <div>
            <div className={`{className ?? ''}`}>
               {`Hello, ${name}!`}
            </div>
            {children}
        </div>
    );
};

Угадайте, красная волнистая линия снова наносит удар!

Свойство className не существует для типа PropsWithChildren‹HelloProps›

Теперь мы знаем, что это значит и как это исправить — нам нужно добавить className к нашим свойствам. Но подождите, это тоже похоже на очень стандартную вещь типа хлеба с маслом (!) для компонентов React. Разве нет стандартного типа, чтобы сказать, что мы хотели бы className, или style, или id, или любой другой такой реквизит, который принимается элементами div HTML?

Рад, что вы спросили, потому что есть. Он называется HTMLAttributes.

Но он не работает сам по себе — нам также нужно сообщить ему, какой это HTML-элемент, чьи HTML-атрибуты мы говорим. В нашем случае мы говорим о стандартном HTML div, поэтому мы будем использовать HTMLAttributes<HTMLDivElement>.

import React, { HTMLAttributes, PropsWithChildren } from 'react';
interface HelloProps extends HTMLAttributes<HTMLDivElement> {
    name: string;
}
const Hello: React.FC<PropsWithChildren<HelloProps>> = ({
    name,
    children,
    ...rest
}) => {
    return (
        <div>
            <div {...rest}>{`Hello, ${name}!`}</div>
            {children}
        </div>
    );
};

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

В конце концов вы столкнетесь с более сложными случаями — скажем, вместо ванильного div вы хотите создать что-то, что ведет себя как HTML-ссылка (<a> — тег привязки). Но вы не будете долго сбиваться с толку, так как теперь вы знаете, что вам просто нужно найти эквивалент HTMLDivElement для тега привязки и использовать его вместо этого. И эти типы имеют довольно красивые имена, так что обычно это тип, имя которого вы можете угадать (например, для тега <a> это HTMLAttributes<HTMLAnchorElement>).

Надеюсь, это было полезно! Если вы хотите узнать больше о наших приключениях с TypeScript, React и другими подобными вещами, следите за нами в Твиттере или заходите на наш прекрасный Discord.

Скажите в следующий раз, счастливого набора текста! 🤓