20 января 2021

⚛️ 12 советов по внедрению TypeScript в React-приложениях

Более 7 лет работы в IT-сфере. Системное администрирование, frontend разработка, написание скриптов на Python.
React и TypeScript – две потрясающие технологии, используемые в наши дни множеством разработчиков. В этой статье мы постарались собрать лучшие практики внедрения TypeScript в React-приложениях.
⚛️ 12 советов по внедрению TypeScript в React-приложениях

Зачем использовать TypeScript в React?

React – это JavaScript-библиотека с открытым исходным кодом для создания пользовательских интерфейсов, а TypeScript – типизированное надмножество JavaScript. Объединяя их, мы создаем пользовательские интерфейсы, используя типизированную версию JavaScript. Это означает большую безопасность и меньшее количество ошибок, отправляемых во внешний интерфейс.

Не существует единственного правильного способа написания кода React с использованием TypeScript. Как и в случае с другими технологиями, если ваш код компилируется и работает, вы, вероятно, что-то сделали правильно.

Подготовка компонентов к совместному использованию с помощью TypeScript

<span>просмотр общих компонентов React в bit.dev</span>
просмотр общих компонентов React в bit.dev

Bit.dev стал популярной альтернативой традиционным библиотекам компонентов, поскольку он предлагает способ собрать и поделиться отдельными компонентами из любой кодовой базы.

Создавая проекты с использованием React с TS, вы убедитесь, что ваши компоненты легко понятны другим разработчикам. Это отличный способ написать поддерживаемый код и оптимизировать совместную работу команды.

Настройка проекта и tsconfig.json

Самый быстрый способ запустить приложение React/TypeScript – использовать create-react-app с шаблоном TypeScript:

        npx create-react-app my-app --template typescript
    

tsconfig.json это файл конфигурации TypeScript. Файл содержит первоначальные настройки, ниже приведем несколько параметров с пояснениями:

tsconfig.json
        {

  "compilerOptions": {

    "target": "es5", // Укажите целевую версию ECMAScript

    "lib": [

      "dom",

      "dom.iterable",

      "esnext"

    ], // Список файлов библиотеки для включения в компиляцию

    "allowJs": true, // Разрешить компиляцию файлов JavaScript

    "skipLibCheck": true, // Пропустить проверку типов всех файлов объявлений

    "esModuleInterop": true, // Отключает импорт пространства имен (импорт * как fs из "fs") и включает импорт в стиле CJS / AMD / UMD (импорт fs из "fs")

    "allowSyntheticDefaultImports": true, // Разрешить импорт по умолчанию из модулей без экспорта по умолчанию

    "strict": true, // Включить все параметры строгой проверки типов

    "forceConsistentCasingInFileNames": true, // Запрещаем ссылки с несогласованным регистром на один и тот же файл.

    "module": "esnext", // Указываем генерацию кода модуля

    "moduleResolution": "node", // Разрешить модули в стиле Node.js

    "isolatedModules": true, // Безоговорочно генерировать импорт для неразрешенных файлов

    "resolveJsonModule": true, // Включить модули, импортированные с расширением .json

    "noEmit": true, // Не выводить вывод (то есть не компилировать код, а только выполнять проверку типа)

    "jsx": "react", // Поддержка JSX в файлах .tsx

    "sourceMap": true, // Создание соответствующего файла .map

    "declaration": true, // Создаем соответствующий файл .d.ts

    "noUnusedLocals": true, // Сообщать об ошибках на неиспользуемых локальных объектах

    "noUnusedParameters": true, // Сообщаем об ошибках неиспользуемых параметров

    "incremental": true, // Включить инкрементную компиляцию путем чтения/записи информации из предыдущих компиляций в файл на диске

    "noFallthroughCasesInSwitch": true // Сообщать об ошибках для случаев падения в инструкции switch

  },

  "include": [

    "src/**/*" // *** Файлы TypeScript должны ввести проверку ***

  ],

  "exclude": ["node_modules", "build"] //  *** Файлы, которые не нужно вводить, проверять ***

}
    

Дополнительные рекомендации исходят от сообщества response-typescript-cheatsheet, а объяснения взяты из документации по параметрам компилятора в официальном справочнике TypeScript.

Enums

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

        //...

/** A set of groupped constants */

enum SelectableButtonTypes {

    Important = "important",

    Optional = "optional",

    Irrelevant = "irrelevant"

}

interface IButtonProps {

    text: string,

    /** The type of button, pulled from the Enum 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>)

}

/** Exporting the component AND the Enum */

export { ExtendedSelectableButton, SelectableButtonTypes}
    

Использование Enums:

        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;
    

Интерфейсы и типы

Что следует использовать – интерфейсы или псевдонимы типов? Хотя эти сущности концептуально различны, на деле они очень похожи:

  • обе могут быть продлены;
        //расширение интерфейсов

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; };
    
  • обе могут использоваться для определения формы объектов;
        //определяем интерфейс для объектов

interface Point {

  x: number;

  y: number;

}

//также используем типы

type Point2 = {

  x: number;

  y: number;

};
    
  • обе они могут быть реализованы одинаково;
        //реализация интерфейса

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;

}
    

Единственная функция интерфейсов, которую не делают псевдонимы типов – это объединение деклараций.

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

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

        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>)

}
    

Типы событий

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

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

Мы также можем использовать для ограничения элементов Generics с конкретным обработчиком событий.

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

        / ** Это позволит вам использовать этот обработчик событий как для якорей, так и для элементов кнопок * /

function eventHandler(event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) {

    console.log("TEST!")

}
    

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

Стоит упомянуть файлы index.d.ts и global.d.ts в React. Оба они устанавливаются, когда вы добавляете React в свой проект. Эти файлы содержат определения типов и интерфейсов: чтобы понять свойства или доступность конкретного типа, вы можете открыть эти файлы и просмотреть их содержимое.

<span> index.d.ts</span>
index.d.ts

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

ESLint/Prettier

Чтобы гарантировать, что ваш код соответствует правилам проекта, а стиль согласован, рекомендуется настроить ESLint и Prettier:

  • Установите необходимые зависимости для разработчиков:
        yarn add eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react --dev
    
  • Создайте файл .eslintrc.js в корне и добавьте следующее:
.eslintrc.js
        module.exports =  {

  parser:  '@typescript-eslint/parser',  // Указывает парсер ESLint

  extends:  [

    'plugin:react/recommended',  // Использует рекомендуемые правила из @eslint-plugin-react

    'plugin:@typescript-eslint/recommended',  // Использует рекомендуемые правила из @typescript-eslint/eslint-plugin

  ],

  parserOptions:  {

  ecmaVersion:  2018,  //Позволяет анализировать современные функции ECMAScript

  sourceType:  'module',  //Разрешает использование импорта

  ecmaFeatures:  {

    jsx:  true,  // Разрешает анализ JSX

  },

  },

  rules:  {

    // Место для указания правил ESLint. Может использоваться для перезаписи правил, указанных в расширенных конфигах

    // например "@ typescript-eslint / явный-возвращаемый-тип-функции": "выкл.",

  },

  settings:  {

    react:  {

      version:  'detect',  // Указывает eslint-plugin-react автоматически определять версию React для использования

    },

  },

};
    
  • Добавьте зависимости Prettier:
        yarn add prettier eslint-config-prettier eslint-plugin-prettier --dev
    
  • Создайте файл .prettierrc.js в корне и добавьте в него следующий код:
.prettierrc.js
        module.exports =  {

  semi:  true,

  trailingComma:  'all',

  singleQuote:  true,

  printWidth:  120,

  tabWidth:  4,

};
    
  • Обновите файл .eslintrc.js:
.eslintrc.js
        module.exports =  {

  parser:  '@typescript-eslint/parser',  // Задает парсер ESLint

  extends:  [

    'plugin:react/recommended',  // Использует рекомендуемые правила из @ eslint-plugin-react

    'plugin:@typescript-eslint/recommended',  // Использует рекомендуемые правила из @typescript-eslint/eslint-plugin

+   'prettier/@typescript-eslint',  // Использует eslint-config-prettier для отключения правил ESLint из @typescript-eslint/eslint-plugin, которые будут конфликтовать с prettier

+   'plugin:prettier/recommended',  // Включает eslint-plugin-prettier и отображает более красивые ошибки как ошибки ESLint. Убедитесь, что это всегда последняя конфигурация в массиве extends.

  ],

  parserOptions:  {

  ecmaVersion:  2018,  // Позволяет анализировать современные функции ECMAScript

  sourceType:  'module',  // Разрешает использование импорта

  ecmaFeatures:  {

    jsx:  true,  //Разрешает анализ JSX

  },

  },

  rules:  {

     // Место для указания правил ESLint. Может использоваться для перезаписи правил, указанных в расширенных конфигах

    // например "@typescript-eslint/явный-тип-возврата-функции": "выкл.",

  },

  settings:  {

    react:  {

      version:  'detect',  // Указывает eslint-plugin-react автоматически определять версию React для использования

    },

  },

};
    

Расширения и настройки кода VS

Следующий шаг по улучшению – автоматическое исправление и предварительная настройка кода при сохранении.

Установите ESLint и Prettier для VS Code. Это позволит ESLint легко интегрироваться с вашим редактором.

Затем обновите настройки, добавив следующий код в свой .vscode/settings.json:

.vscode/settings.json
        {

    "editor.formatOnSave": true

}
    

Хуки

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

Вы можете принудительно установить тип или интерфейс начального значения состояния, например, так:

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

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

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

// later...

setUser(newUser);
    

Обработка событий формы

Один из наиболее распространенных случаев – это правильный ввод using в поле ввода onChange. Вот пример:

        import React from 'react'

const MyInput = () => {

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

  // The event type is a "ChangeEvent"

  // We pass in "HTMLInputElement" to the input

  function onChange(e: React.ChangeEvent<HTMLInputElement>) {

    setValue(e.target.value)

  }

  return <input value={value} onChange={onChange} id="input-example"/>

}
    

Расширение свойств компонентов

Можно расширить свойства, объявленные для одного компонента, и задействовать их в другом. Давайте сначала посмотрим, как использовать type:

        import React from 'react';

type ButtonProps = {

    /** the background color of the button */

    color: string;

    /** the text to show inside the button */

    text: string;

}

type ContainerProps = ButtonProps & {

    /** the height of the container (value used with 'px') */

    height: number;

}

const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {

  return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>

}
    

Если вы использовали interface, то можно применить extends, чтобы расширить его, но придется внести пару изменений:

        import React from 'react';

interface ButtonProps {

    /** the background color of the button */

    color: string;

    /** the text to show inside the button */

    text: string;

}

interface ContainerProps extends ButtonProps {

    /** the height of the container (value used with 'px') */

    height: number;

}

const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {

  return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>

}
    

Сторонние библиотеки

Мы часто включаем сторонние библиотеки в проекты React и TypeScript. В таких случаях стоит посмотреть, есть ли пакет @types с определениями типов:

        #yarn

yarn add @types/<package-name>

#npm

npm install @types/<package-name>
    

Пространство имен @types зарезервировано для определений типов пакетов. Они живут в репозитории под названием DefinitherTyped, который частично поддерживается командой TypeScript, а частично – сообществом.

Итоги

Совместное использование React и TypeScript требует некоторого обучения из-за объема информации, но в долгосрочной перспективе затраты времени окупаются. В своей публикации мы опирались на лучшие практики из статей Джо Превайта, Фернандо Дольо и Мартина Хохеля. Мы постарались осветить самые полезные приемы и моменты, которые помогут использовать TypeScript как часть инструментальной цепочки React. Больше информации вы найдете в официальном справочнике к TypeScript или в публикациях «Библиотеки программиста».

Источники

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
C# Developer
Москва, по итогам собеседования
Backend Lead (Python, Django)
по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ