TypeScript + React

Подключение типов React

Существует много способов подключения библиотеки React. Выбор конкретного способа может зависеть от используемых технологий, языков программирования или личных предпочтений. Мы будем рассматривать наиболее популярный на данный момент способ - подключение npm-пакета с React. Npm-пакет с React не содержит типов для TypeScript. Типы для React находятся в отдельном npm-пакете @types/react. Его нужно установить отдельно. Исходные файлы этого пакета хранятся в репозитории DefinitelyTyped.

Импорт типов из модуля

TypeScript-модули могут экспортировать не только конструкции кода, но и типы.

/* SomeComponent.tsx */
export type Align = "left" | "right";

export interface SomeComponentProps {
    align: Align;
    value: string | null;
    onChange: (value: string | null) => void;
}

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

/* OtherComponent.tsx */
import { Align, SomeComponentProps } from "{ path to module }";

const align: Align = "left";

В TypeScript >= 3.8 появилась возможность импортировать типы из другого модуля только на уровне типов. При обычном импорте, скрипт внутри импортируемого модуля исполняется. Когда из модуля импортируются только типы лишнее выполнение скрипта модуля может повлиять на производительность, а также создать неприятные сайд-эффекты. Для использования импорта типов нужно использовать конструкцию import type

/* OtherComponent.tsx */
import type { Align, SomeComponentProps } from "{ path to module }";

const align: Align = "left";

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

import React from "react"

В TypeScript без дополнительных настроек так сделать не получится. Нужно явно указывать alias:

import * as React from "react"

Для TypeScript >= 2.7 в tsconfig.json можно выставить флажок compilerOptions.esModuleInterop: true. Тогда компилятор TypeScript будет генерировать дополнительные функции-хелперы в runtime, которые позволят использовать синтаксис экосистемы Babel. Подробнее об этой возможности можно узнать отдельно. Мы будем использовать явный импорт.

Функциональные компоненты

Долгое время в React функциональный компонент называли Stateless Component, потому что ему нельзя было добавить state. В React 16.8 появилась возможность использовать Hook-и. Это позволило использовать state в таких компонентах. Поэтому для версий React < 16.8 (а точнее для версии типов @types/react) для функциональных компонентов использовался тип StatelessComponent.

const ColorText: React.StatelessComponent = () => {
   return <span>text</span>
}

Тип StatelessComponent имеет один generic-параметр <P = {}>, которому можно указать тип props-ов. Тип props-ов обычно создается отдельно в виде интерфейса. Использование интерфейса позволит использовать наследование, что упростит переиспользование компонента.

interface ColorTextProps {
   color: "red" | "green" | "blue";
}

const ColorText: React.StatelessComponent<ColorTextProps> = (props) => {
   return <span style={{color: props.color}}>text</span>
}

В типах React есть тип React.SFC<P = {}> = React.StatelessComponent<P>. Тип SFC (stateless functional component) - это alias к типу StatelessComponent, который нужен для уменьшения длины строки.

interface ColorTextProps {
   color: "red" | "green" | "blue";
}

const ColorText: React.SFC<ColorTextProps> = (props) => {
   return <span style={{color: props.color}}>text</span>
}

В интерфейсах props-ов не нужно объявлять поле children. Оно уже объявлено в типе StatelessComponent. Даже если используется дефолтный generic для props, поле children все равно доступно.

interface ColorTextProps {
   color: "red" | "green" | "blue";
}

const ColorText: React.SFC<ColorTextProps> = (props) => {
   return <span style={{color: props.color}}>{props.children}</span>
}

В React >= 16.8 тип StatelessComponent заменяется на FunctionComponent, а SFC заменяется на FC. StatelessComponent и SFC не были удалены из типов React-а, но были помечены меткой @deprecated. С точки типов StatelessComponent<P = {}> = FunctionComponent<P>. То есть разница между этими двумя типами только в названии. Hook-и можно использовать не зависимо от того, какой тип вы используете.

interface ColorTextProps {
   color: "red" | "green" | "blue";
}

const ColorText: React.FC<ColorTextProps> = (props) => {
   return <span style={{color: props.color}}>{props.children}</span>
}

Более того, для объявления функционального компонента вообще не обязательно использовать ни FunctionComponent, ни FC. Достаточно объявить функцию, которая будет возвращать объект типа React.ReactElement | null.

interface MdashProps {
   color: "red" | "green" | "blue";
}

const Mdash = (props: MdashProps): React.ReactElement | null => {
   return <span style={{color: props.color}}>&mdash;</span>
}

или

interface MdashProps {
   color: "red" | "green" | "blue";
}

function Mdash(props: MdashProps): React.ReactElement {
   return <span style={{color: props.color}}>&mdash;</span>
}

В таком подходе в props нужно явно объявлять children. По умолчанию тип у children?: React.ReactNode.

interface ColorTextProps {
   color: "red" | "green" | "blue";
   children?: React.ReactNode
}

function ColorText(props: ColorTextProps): React.ReactElement {
     return <span style={{color: props.color}}>{props.children}</span>
}

Тип для children можно заменить на любой другой. Например, его можно сделать обязательным или сделать его функцией. Для этого нужно указать в props поле children с нужным типом. Это будет работать и при использовании типов FunctionComponent и FC. Если при использовании компонента тип передаваемых children будет отличаться от типа, объявленного в props-ах - TypeScript выдаст ошибку.

interface ColorTextProps {
    color: "red" | "green" | "blue";
    children: () => React.ReactNode;
}

const ColorText: React.FC<ColorTextProps> = (props) => {
    return <span style={{ color: props.color }}>{props.children()}</span>
}

function App() {
    return (
        <ColorText color="green">
            some text //Error: 'ColorText' components don't accept text as child elements. Text in JSX has the type 'string', but the expected type of 'children' is '(() => ReactNode)
        </ColorText>
    )
}

В коде выше у функции App не указано возвращаемое значение. TypeScript автоматически выводит возвращаемое значение React.ReactElement у функциональных компонентов - поэтому тип можно не указывать.

У всех Hook-ов есть свои собственные типы. Например тип у Hook-а useState такой:

type SetStateAction<S> = S | ((prevState: S) => S);
type Dispatch<A> = (value: A) => void;
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];

Чаще всего TypeScript может автоматически вывести generic S по заданному начальному значению.

const [value, setValue] = React.useState(10);

Однако автоматический вывод не всегда возможен или не всегда подходит. Например, если в качестве начального значения используется null - не ясно какой тип автоматически выводить. Или, например, нужно использовать литеральный тип. В таких случаях можно задавать generic-параметр явно.

const [color, setColor] = React.useState<"red" | "green" | "blue">("red");
const [text, setText] = React.useState<string>(null);

Компоненты-классы

Для создания компонента-класса нужно наследовать класс компонента от React.Component. Тип React.Component имеет 2 generic-параметра: первый для типа props, второй для типа state.

interface ColorTextProps {
   value: string | null;
   onChange: (value: string | null) => void;
}

interface ColorTextState {
   color: "red" | "green" | "blue";
}

class ColorText extends React.Component<ColorTextProps, ColorTextState> {
   ...
}

У этих generic-параметров есть дефолтные значения {}, поэтому их можно не указывать.

class Comp1 extends React.Component {
   ...
}

class Comp2 extends React.Component<ColorTextProps> {
   ...
}

// Мы не можем указать 2-ой generic не указав 1-ый
class Comp3 extends React.Component<{}, ColorTextState> {
   ...
}

Также как и для функциональных компонентов в типе props не нужно указывать поле children. Это поле можно указать, чтобы переопределить тип.

Метод render() компонента-класса должен возвращает тип React.ReactNode. Этот тип позволяет возвращать не только элементы React и null, но и string, number, boolean и undefiend.

class SomeComponent extends React.Component<{value: any}> {
   render(): React.ReactNode {
      const value = this.props.value;
      if(typeof value === "number") {
         return value + 10; // number
      }
      if(typeof value === "boolean") {
         return !value; // boolean
      }
      if(typeof value === "undefined") {
         return value; // undefiened
      }

      return JSON.stringify(value); // string
   }
}

Для типизации параметров методов жизненного цикла компонента используются те же типы, которые были объявлены для generic-параметров тип React.Component.

interface ColorTextProps {
   value: string;
}

interface ColorTextState {
   color: "red" | "green" | "blue";
}

class ColorText extends React.Component<ColorTextProps, ColorTextState> {
   componentDidUpdate(prevProps: ColorTextProps, prevState: ColorTextState): void {
      if(this.props.value !== prevProps.value || this.state.color !== prevState.color) {
         ...
      }
   }
   ...
}

Специально для работы с React в TypeScript сделали особую поддержку для deafultProps. Если у какого-то props-а есть дефолтные значение, то этот props указывать не обязательно. В таком случае в типе props-ов это поле можно пометить как необязательное:

interface ColorTextProps {
   value: string;
   width?: number;
}

Однако, необязательному полю можно в качестве значения передать undefined, что противоречит его типу. Поэтому разработчики TypeScript добавили возможность не помечать ? те props-ы, которые есть в deafultProps. При этом во время использования компонента такие поля можно не указывать.

interface ColorTextProps {
   value: string;
   width: number;
}

class ColorText extends React.Component<ColorTextProps> {
   static defaultProps = {
      width: 100
   }
   ...
}

function App() {
   return (
      <ColorText value="value">Some message</ColorText>
   )
}

Тип компонента

Иногда возникает необходимость использовать тип компонента. Например, для написания HOC-ов. Для этого в типах React есть специальные типы. Один из них - это React.ComponentType. В него входят тип функционального компонента и компонента-класса

function myHOC(Component: React.ComponentType) {
   ...
}

class Some extends React.Component {
    render() {
        return null
    }
}

myHOC(() => <div>lalaka</div>);
myHOC(Some);

Тип React.ComponentType имеет generic-параметр. Этот generic описывает тип props-ов. По умолчанию он равен {}. Поэтому, если в HOC myHOC передать React компонент с типом props-ов отличных от {} - TypeScript выдаст ошибку.

const Text = (props: { value: string }) => {
    return <span>{props.value}</span>;
}

myHOC(Text); // Error: Argument of type '(props: { value: string; }) => JSX.Element' is not assignable to parameter of type 'ComponentType<{}>'.

Для того, чтобы сделать HOC, который принимает компоненты с любым типом props-ов можно использовать any. Но лучше использовать generic-параметр и автоматический вывод типов generic-ов.

function myHOC<TProps>(Component: React.ComponentType<TProps>) {
   ...
}

myHOC(Text);

В коде выше generic TProps автоматически выводится TypeScript как { value: string }, когда в myHOC передается компонент Text. Используя generic для типов props-ов, можно накладывать ограничения на компоненты, которые можно передавать в HOC. Для этого нужно накладывать ограничение на тип props-ов.

interface WithColor {
    color: string;
}

function myHOC<TProps extends WithColor>(Component: React.ComponentType<TProps>) {
   ...
}

const Text = (props: { value: string }) => {
    return <span>{props.value}</span>;
}

const ColorText = (props: { value: string, color: string }) => {
    return <span style={{color: props.color}}>{props.value}</span>;
}

myHOC(ColorText);
myHOC(Text); // Error: Argument of type '(props: { value: string; }) => JSX.Element' is not assignable to parameter of type 'ComponentType<WithColor>'.

В коде выше в HOC myHOC можно передавать только те компоненты, тип props-ов которых содержит поле color: string;.

Last updated