Я кодирую на React уже более 5 лет. Сначала, когда я начинал, мне всегда казалось, что чего-то не хватает. В самом React нет строгой проверки типов, и в крупномасштабных проектах вы не будете знать, с какой формой состояния вы работаете или что возвращает таинственный ответ API (если вы его не проверите). Обычно требуется больше времени для кодирования некоторой функции (странно, поскольку вы пишете больше кода с TypeScript, а не без него). Также иногда вы сталкиваетесь с ошибками, когда пытаетесь использовать какое-то значение, но его там нет.

TypeScript решает эти проблемы! Я напишу сводку полезных фрагментов кода с помощью React + TypeScript.

1. Функциональная (презентационная) составляющая

import React, { MouseEvent } from 'react'
interface Props {
  onClick(e: MouseEvent<HTMLElement>): void
}
const Button: React.FC<Props> = ({ onClick, children }) => (
  <button>
    {children}
  </button>
)

В @types/react = ›type FC<P> есть предопределенный тип, который является просто псевдонимом interface FunctionalComponent<P>, и для него есть предопределенный children и некоторые другие вещи (defaultProps, displayName ...) , поэтому нам не нужно писать его каждый раз самостоятельно!

2. Компонент с отслеживанием состояния

Прежде всего мы объявляем наше initialState:

const initialState = { counter: 0 };

Затем мы используем Typescript, чтобы вывести тип состояния из нашей реализации.

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

type State = Readonly<typeof initialState>

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

readonly state: State = initialState

Почему это полезно / необходимо?

Мы знаем, что не можем обновить state непосредственно в React, как показано ниже:

this.state.clicksCount = 55
this.state = { clicksCount: 55 }

Это вызовет ошибку времени выполнения, но не во время компиляции. Путем явного сопоставления нашего type State со значением только для чтения через Readonly и установки состояния «только для чтения» в нашем компоненте класса TS сразу же сообщит нам, что мы делаем что-то не так.

Пример:

Итоговый компонент выглядит так:

import React, { Component, MouseEvent } from 'react';
const initialState = { clicksCount: 0 };
type State = Readonly<typeof initialState>;
class ButtonCounter extends Component<object, State> {
  readonly state: State = initialState;
  render() {
    const { clicksCount } = this.state;
    return (
      <>
        <Button onClick={this.handleIncrement}>Add</Button>
        <Button onClick={this.handleDecrement}>Decrease</Button>
        Counter is: {clicksCount}!
      </>
    );
  }
  handleIncrement = () => this.setState(incrementCounter);
  handleDecrement = () => this.setState(decrementCounter);
}
const incrementCounter = (prevState: State) => ({ clicksCount: prevState.clicksCount + 1 });
const decrementCounter = (prevState: State) => ({ clicksCount: prevState.clicksCount - 1 });

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

const decrementClicksCount = (prevState: State) 
                      => ({ clicksCount: prevState.clicksCount-- })
// Throws complile error:
//
// [ts]
// Cannot assign to 'clicksCount' because it is a constant or a read-only property.

3. Компонент высшего порядка (энхансеры)

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

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

Сначала мы определяем интерфейс:

interface WithLoadingProps {
  loading: boolean;
}

Затем мы будем использовать общий; P представляет свойства компонента, который передается в HOC. React.ComponentType<P> - это псевдоним для React.FunctionComponent<P> | React.ClassComponent<P>, означающий, что компонент, который передается в HOC, может быть либо функциональным компонентом, либо компонентом класса.

<P extends object>(Component: React.ComponentType<P>)

Затем мы определяем компонент, который должен возвращаться из HOC, и указываем, что компонент будет включать в себя переданные свойства компонента (P) и свойства HOC (WithLoadingProps). Для объединения нескольких типов мы используем оператор пересечения типов (&).

class WithLoading extends React.Component<P & WithLoadingProps>

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

return loading ? <LoadingSpinner /> : <Component {...props as P} />;

Это последняя реализация класса:

const withLoading = <P extends object>(Component: React.ComponentType<P>) =>
  class WithLoading extends React.Component<P & WithLoadingProps> {
    render() {
      const { loading, ...props } = this.props;
      return loading ? <LoadingSpinner /> : <Component {...props as P} />;
    }
  };
class WithLoading extends React.Component<P & WithLoadingProps>

Также это может быть функциональная составляющая:

const withLoading = <P extends object>(
  Component: React.ComponentType<P>
): React.FC<P & WithLoadingProps> => ({
  loading,
  ...props
}: WithLoadingProps) =>
  loading ? <LoadingSpinner /> : <Component {...props as P} />;

Теперь мы можем обернуть наш ButtonCounter (Функциональный или Классный) и в результате получить LoadingButtonCounter. В будущем, если мы решим, что нам понадобится другой компонент Загрузка, нам не нужно будет создавать логику загрузки с нуля. Мы просто повторно используем наш withLoading HOC. Это экономит время, поверьте!

4. Компонент высшего порядка (форсунки)

Наиболее распространенной формой HOC являются форсунки . Однако задать для них типы сложнее. Помимо внедрения props в компонент, в большинстве случаев они также удаляют введенные props, когда он обернут, чтобы их больше нельзя было установить снаружи. connect в response-redux является примером инжектора HOC, но в этой статье мы будем использовать более простой пример - HOC, который вводит значение счетчика и выполняет обратные вызовы для увеличения и уменьшения значения:

Здесь есть несколько основных отличий:

export interface InjectedCounterProps {  
  value: number;  
  onIncrement(): void;  
  onDecrement(): void;
}

Объявляется интерфейс для свойств, которые будут внедрены в компонент. Он экспортируется, поэтому его может использовать компонент, обернутый HOC:

<P extends InjectedCounterProps>(Component: React.ComponentType<P>)

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

class CounterFactory extends React.Component<
  Subtract<P, InjectedCounterProps>,    
  CounterFactoryState  
>

Компонент, возвращаемый HOC, использует Subtract из пакета utility-types, который вычитает введенные реквизиты из переданных в реквизитах компонента, что означает, что если они будут установлены для полученного в результате обернутого компонента, вы получите ошибку компиляции:

const WrappedCounter = counterFactory(Counter);
const wrappedCounter = <WrappedCounter value={2} /> // TS error setting value

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

Спасибо за чтение! Дайте мне знать, что вы думаете.