Подготовка к работе

create-react-app с TypeScript

$ npx create-react-app your-app-name --template typescript

Если вы предпочитаете Yarn, используйте следующую команду:

$ yarn create react-app your-app-name --template typescript

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

Основы

Интерфейсы

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

import React from 'react';

interface IButtonProps {
    /** Текст внутри кнопки */
    text: string,
    /** Тип кнопки, извлеченный из перечисления ButtonTypes */
    type: ButtonTypes,
    /** Функция, выполняемая после нажатия кнопки */
    action: () => void
}

const ExtendedButton : React.FC<IButtonProps> = ({text, type, action}) => {

}

В приведенный выше код необходимо добавить 3 свойства:

  • text: представлен String.
  • type: представлен опцией ButtonType.
  • action: простая функция.

Обратите внимание, что мы «расширили» тип FC (функциональный компонент) с помощью собственного пользовательского интерфейса. Благодаря этому функция получает все общие определения функциональных компонентов, такие как prop и тип return, которые должны быть присвоены JSX.Element.

Если вы проигнорируете одно из них или отправите несовместимое значение, компилятор TypeScript и IDE (при условии, что вы используете специфичную для JavaScript IDE, такую как Code) уведомят вас об этом. Вы сможете продолжить работу только после исправления ошибки.

Лучший способ определить элемент ExtendedButton — расширить нативный тип HTML-элемента button следующим образом:

import React, {ButtonHTMLAttributes} from 'react';

interface IButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
    /** Текст внутри кнопки */
    text: string,
    /** Тип кнопки, извлеченный из перечисления ButtonTypes */
    type: ButtonTypes,
    /** Функция, выполняемая после нажатия кнопки */
    action: () => void
}

const ExtendedButton : React.FC<IButtonProps> = ({text, type, action}) => {

}

Также обратите внимание, что при работе с Bit.dev или react-docgen для автоматической генерации документов потребуется следующий синтаксис:

const ExtendedButton : React.FC<IButtonProps> = ({text, type, action} : IButtonProps) => {

}

(Прямое определение props можно выполнить с помощью: IButtonProps в дополнение к определению компонента с :React.FC<IButtonProps>)

Перечисления (Enums)

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

//...
/** Набор сгруппированных констант */
enum SelectableButtonTypes {
    Important = "important",
    Optional = "optional",
    Irrelevant = "irrelevant"
}

interface IButtonProps {
    text: string,
    /** Тип кнопки, извлеченный из перечисления SelectableButtonTypes */
    type: SelectableButtonTypes,
    action: (selected: boolean) => void
}

const ExtendedSelectableButton = ({text, type, action}: IButtonProps) => {
    let [selected, setSelected]  = useState(false)

    return (<button className={"extendedSelectableButton " + type + (selected? " selected" : "")} onClick={ _ => {
        setSelected(!selected)
        action(selected)
    }}>{text}</button>)
}

/** Экспорт компонента и перечисления */
export { ExtendedSelectableButton, SelectableButtonTypes}

Импорт и использование перечислений:

import React from 'react';
import './App.css';
import {ExtendedSelectableButton, SelectableButtonTypes} from './components/ExtendedSelectableButton/ExtendedSelectableButton'

const App = () => {
  return (
    <div className="App">
      <header className="App-header">
        <ExtendedSelectableButton type={SelectableButtonTypes.Important} text="Select me!!" action={ (selected) => {
          console.log(selected) 
        }} />       
        
      </header>
    </div>
  );
}

export default App;

Обратите внимание, что в отличие от интерфейсов или типов, перечисления переводятся на простой JavaScript. Например:

enum SelectableButtonTypes {

Important = "important",

Optional = "optional",

Irrelevant = "irrelevant"

}

Код выше преобразуется в следующее:

"use strict";

var SelectableButtonTypes;

(function (SelectableButtonTypes) {

SelectableButtonTypes["Important"] = "important";

SelectableButtonTypes["Optional"] = "optional";

SelectableButtonTypes["Irrelevant"] = "irrelevant";

})(SelectableButtonTypes || (SelectableButtonTypes = {}));

Интерфейсы и псевдонимы типов

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

Telegram-канал по теме
NOP::HumorИТ-юмор и мемы о боли разработчиков
Подписаться

Несмотря на то, что теоретически эти сущности различны, на практике они очень схожи:

  1. Могут расширяться.
//расширенные интерфейсы
interface PartialPointX { x: number; }
interface Point extends PartialPointX { y: number; }

//расширенные типы
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

//интерфейс расширяет тип 
type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }

//псевдоним типов расширяет интерфейсы
interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };

2. Могут использоваться для определения формы объектов.

//определение интерфейса для объектов
interface Point {
  x: number;
  y: number;
}

//использование типов
type Point2 = {
  x: number;
  y: number;
};

3. Могут быть реализованы одинаково.

//реализация интерфейса
class SomePoint implements Point {
  x: 1;
  y: 2;
}

//реализация псевдонима типа
class SomePoint2 implements Point2 {
  x: 1;
  y: 2;
}

type PartialPoint = { x: number; } | { y: number; };

// Единственное, что нельзя выполнить: реализовать тип объединения.
class SomePartialPoint implements PartialPoint {
  x: 1;
  y: 2;
}

Единственная дополнительная функция интерфейсов — это «объединение описаний», т. е. вы можете определить один и тот же интерфейс несколько раз, и с каждым определением свойства объединяются:

interface Point { x: number; } //описание #1
interface Point { y: number; } //описание #2

// Оба описания становятся:
// interface Point { x: number; y: number; }
const point: Point = { x: 1, y: 2 };

Дополнительные типы для props

К преимуществам использования интерфейсов можно отнести возможность применять свойства props для компонентов. Тем не менее, благодаря дополнительному синтаксису, доступному в TypeScript, можно также определить дополнительные props. Например:


//...
interface IProps {
  prop1: string,
  prop2: number, 
  myFunction: () => void,
  prop3?: boolean //optional prop
}

//...
function MyComponent({...props}: IProps) {
  //...
}

/** Затем их можно использовать следующим образом */
<mycomponent prop1="text here" prop2=404 myFunction={() = {
  //...
}} />

<mycomponent prop1="text here" prop2={404} myFunction={() = {
  //...
}}  prop3={false} />

Хуки

Хуки — это новый механизм React для взаимодействия с некоторыми функциями (например, состоянием) без необходимости определять класс.

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

Такие хуки, как useState, получают параметр, возвращают состояние и функцию для его установки.

Благодаря проверке типа в TypeScript можно реализовать тип (или интерфейс) начального значения состояния следующим образом:


const [user, setUser] = React.useState<IUser>(user);

Нулевые значения для хуков

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

const [user, setUser] = React.useState<IUser | null>(null);

// позже...
setUser(newUser);

Таким образом, вы не только сохраняете проверки типов, но и учитываете те сценарии, в которых начальное значение может быть равно null.

Общие компоненты

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

interface Props<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>(props: Props<T>) {
  const { items, renderItem } = props;
  const [state, setState] = React.useState<T[]>([]); 
  
  return (
    <div>
      {items.map(renderItem)}
    </div>
  );
}

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

Пример вывода типа:

ReactDOM.render(
  <List
    items={["a", "b"]} // выведенный тип 'string'
    renderItem={item => (
      <li key={item}>
        {item.trim()} //допустимо, поскольку мы работаем с 'strings'
      </li>
    )}
  />,
  document.body
);

Объявленные напрямую типы:

ReactDOM.render(
  <List<number>
    items={[1,2,3,4]} 
    renderItem={item => <li key={item}>{item.toPrecision(3)}</li>}
  />,
  document.body
);

Обратите внимание: если в последнем примере список содержит строки вместо цифр, то TypeScript выдает ошибку во время процесса транспиляции.

Расширение HTML-элементов

Иногда компоненты функционируют и ведут себя как нативные HTML-элементы. Например, «border-box» (компонент, который отображает div с рамкой по умолчанию) или «big submit» (старая добрая кнопка отправки с размером по умолчанию и некоторым пользовательским поведением).

Для этих сценариев лучше всего определить тип компонента как нативный HTML-элемент или его расширение.

export interface IBorderedBoxProps extends React.HTMLAttributes<HTMLDivElement> {
    title: string;
}

class BorderedBox extends React.Component<IBorderedBoxProps, void> {
    public render() {
        const {children, title, ...divAttributes} = this.props;

        return (
            //Это DIV, и мы пытаемся предоставить пользователю эту информацию.
            <div {...divAttributes} style={{border: "1px solid red"}}>
                <h1>{title}</h1>
                {children}
            </div>
        );
    }
}

const myBorderedBox = <BorderedBox title="Hello" onClick={() => alert("Hello")}/>;

В коде выше я расширил HTML props по умолчанию и добавил новое: «title».

Типы событий

React предоставляет собственный набор событий, поэтому использовать старые добрые HTML-события напрямую не получится. При этом у вас есть доступ ко всем необходимым UI-событиям. Они имеют те же имена, поэтому убедитесь, что ссылаетесь на них напрямую, как React.MouseEvent, или просто импортируйте их из React:

import React, { Component, MouseEvent } from 'react';

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

Например, следующий код не будет работать:


function eventHandler(event: React.MouseEvent<HTMLAnchorElement>) {
    console.log("TEST!")
}

const ExtendedSelectableButton = ({text, type, action}: IButtonProps) => {
    
    let [selected, setSelected]  = useState(false)

    return (<button className={"extendedSelectableButton " + type + (selected? " selected" : "")} onClick={eventHandler}>{text}</button>)
}

И вы получите подобное сообщение об ошибке:

Однако вы можете использовать объединения, чтобы разрешить повторное использование одного обработчика несколькими компонентами:

/** Благодаря этому вы сможете использовать обработчик событий как для элементов anchors, так и для button */
function eventHandler(event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) {
    console.log("TEST!")
}

Интегрированное определение типа

В качестве последнего совета стоит упомянуть файлы index.d.ts и global.d.ts. Они устанавливаются при добавлении React в проект (если вы использовали npm, вы найдете их в папке npm_modules/@types).

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

Например:

Там вы можете увидеть небольшой раздел файла index.d.ts, в котором показаны различные подписи для функции createElement.

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


Перевод статьи Fernando Doglio: React TypeScript: Basics and Best Practices

Мы в Telegram — подписывайтесь
NOP::Nuances of ProgrammingСтатьи и переводы для разработчиков — каждый день
Подписаться
NOP::HumorИТ-юмор и мемы о боли разработчиков
Подписаться
NOP::Recruiter RemoteУдалённые ИТ-вакансии с вилками
Подписаться
Предыдущая статьяGo: стратегия встраивания и ограничение
Следующая статьяПростейшее объяснение парадокса Монти Холла