React с TypeScript: Руководство

React и TypeScript — две замечательные технологии, используемые сегодня многими разработчиками. Знание того, как что-то делать, может быть сложным, и иногда трудно найти правильный ответ. Не беспокойтесь. Мы собрали лучшие практики вместе с примерами, чтобы развеять любые сомнения, которые могут у вас возникнуть.

Как React и TypeScript работают вместе

Прежде чем мы начнем, давайте еще раз посмотрим, как React и TypeScript работают вместе. React — это «библиотека JavaScript для создания пользовательских интерфейсов», а TypeScript — «типизированный надмножество JavaScript, который компилируется в простой JavaScript».

Используя их вместе, мы, по сути, создаем наши пользовательские интерфейсы, используя типизированную версию JavaScript.

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

Компилирует ли TypeScript мой код React?

Распространенный вопрос, на который не будет лишним ответить, — компилирует ли TypeScript ваш код React.

Ответ - да, это так! Но позже, когда мы рассмотрим tsconfig.json настройки, большую часть времени вы захотите использовать файлы "noEmit": true. Это означает, что TypeScript не будет выдавать JavaScript после компиляции. Это потому, что обычно мы используем TypeScript только для проверки типов.

Вывод обрабатывается в настройках CRA с помощью react-scripts. Мы запускаем yarn build и react-scripts объединяем результаты для production.

Напомним, что TypeScript компилирует ваш код React для проверки кода. Он не выдает никакого вывода JavaScript (в большинстве сценариев). Вывод по-прежнему похож на проект React без TypeScript.

Может ли TypeScript работать с React и webpack?

Да, TypeScript может работать с React и webpack. К счастью для вас, в документации по webpack есть руководство по этому вопросу.

Надеемся, это даст вам небольшое освежение в том, как они работают вместе. Теперь о лучших практиках!

Лучшие практики

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

Конфигурация

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

  • tsconfig.json
  • ESLint
  • Prettier
  • Расширения и настройки VS Code.

Настройка проекта

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

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

Это даст вам необходимый минимум, чтобы начать писать React с помощью TypeScript. Несколько заметных отличий:

  • .tsxрасширение файла
  • tsconfig.json
  • react-app-env.d.ts

tsx для «TypeScript JSX ». Tsconfig.json файл конфигурации TypeScript, в котором установлены некоторые значения по умолчанию. react-app-env.d.ts ссылается на типы react-scripts и помогает с такими вещами, как разрешение импорта SVG.

tsconfig.json

К счастью для нас, для нас генерируется последний шаблон React/TypeScript tsconfig.json. Тем не менее, они добавляют необходимый минимум для начала работы. Мы предлагаем вам изменить свой, чтобы он соответствовал приведенному ниже. Мы также добавили комментарии, объясняющие назначение каждой опции:

{
  "compilerOptions": {
    "target": "es5", // Specify ECMAScript target version
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ], // List of library files to be included in the compilation
    "allowJs": true, // Allow JavaScript files to be compiled
    "skipLibCheck": true, // Skip type checking of all declaration files
    "esModuleInterop": true, // Disables namespace imports (import * as fs from "fs") and enables CJS/AMD/UMD style imports (import fs from "fs")
    "allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export
    "strict": true, // Enable all strict type checking options
    "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file.
    "module": "esnext", // Specify module code generation
    "moduleResolution": "node", // Resolve modules using Node.js style
    "isolatedModules": true, // Unconditionally emit imports for unresolved files
    "resolveJsonModule": true, // Include modules imported with .json extension
    "noEmit": true, // Do not emit output (meaning do not compile code, only perform type checking)
    "jsx": "react", // Support JSX in .tsx files
    "sourceMap": true, // Generate corrresponding .map file
    "declaration": true, // Generate corresponding .d.ts file
    "noUnusedLocals": true, // Report errors on unused locals
    "noUnusedParameters": true, // Report errors on unused parameters
    "incremental": true, // Enable incremental compilation by reading/writing information from prior compilations to a file on disk
    "noFallthroughCasesInSwitch": true // Report errors for fallthrough cases in switch statement
  },
  "include": [
    "src/**/*" // *** The files TypeScript should type check ***
  ],
  "exclude": ["node_modules", "build"] // *** The files to not type check ***
}

Дополнительные рекомендации исходят от сообщества react-typescript-cheatsheet , а пояснения — из документации по параметрам компилятора в официальном руководстве по TypeScript. Это замечательный ресурс, если вы хотите узнать о других вариантах и ​​о том, что они делают.

ESLint/Prettier

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

Установите необходимые зависимости для разработчиков:

yarn add eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react --dev

Создайте .eslintrc.js файл в корне и добавьте следующее:

module.exports =  {
  parser:  '@typescript-eslint/parser',  // Specifies the ESLint parser
  extends:  [
    'plugin:react/recommended',  // Uses the recommended rules from @eslint-plugin-react
    'plugin:@typescript-eslint/recommended',  // Uses the recommended rules from @typescript-eslint/eslint-plugin
  ],
  parserOptions:  {
  ecmaVersion:  2018,  // Allows for the parsing of modern ECMAScript features
  sourceType:  'module',  // Allows for the use of imports
  ecmaFeatures:  {
    jsx:  true,  // Allows for the parsing of JSX
  },
  },
  rules:  {
    // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
    // e.g. "@typescript-eslint/explicit-function-return-type": "off",
  },
  settings:  {
    react:  {
      version:  'detect',  // Tells eslint-plugin-react to automatically detect the version of React to use
    },
  },
};

Добавьте зависимости Prettier:

yarn add prettier eslint-config-prettier eslint-plugin-prettier --dev

Создайте .prettierrc.js файл в корне и добавьте следующее:

module.exports =  {
  semi:  true,
  trailingComma:  'all',
  singleQuote:  true,
  printWidth:  120,
  tabWidth:  4,
};

Обновите .eslintrc.js файл:

module.exports =  {
  parser:  '@typescript-eslint/parser',  // Specifies the ESLint parser
  extends:  [
    'plugin:react/recommended',  // Uses the recommended rules from @eslint-plugin-react
    'plugin:@typescript-eslint/recommended',  // Uses the recommended rules from the @typescript-eslint/eslint-plugin
+   'prettier/@typescript-eslint',  // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
+   'plugin:prettier/recommended',  // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
  ],
  parserOptions:  {
  ecmaVersion:  2018,  // Allows for the parsing of modern ECMAScript features
  sourceType:  'module',  // Allows for the use of imports
  ecmaFeatures:  {
    jsx:  true,  // Allows for the parsing of JSX
  },
  },
  rules:  {
    // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
    // e.g. "@typescript-eslint/explicit-function-return-type": "off",
  },
  settings:  {
    react:  {
      version:  'detect',  // Tells eslint-plugin-react to automatically detect the version of React to use
    },
  },
};

Эти рекомендации есть в ресурсе сообщества, написанного Робертом Купером под названием « Использование ESLint и Prettier в проекте TypeScript ». Если вы посетите этот ресурс, вы сможете узнать больше о том, «почему» стоят за этими правилами и конфигурациями.

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

Мы добавили ESLint и Prettier, и следующим шагом по улучшению нашего DX является автоматическое исправление/улучшение нашего кода при сохранении.

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

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

{
    "editor.formatOnSave": true
}

Это позволит VS Code творить чудеса и исправлять ваш код при сохранении. Это красиво!

Компоненты

Одной из основных концепций React являются компоненты. Здесь мы будем ссылаться на стандартные компоненты React v16.8, то есть те, которые используют хуки, а не классы.

В общем, по основным компонентам есть на что обратить внимание. Давайте посмотрим на пример:

import React from 'react'

// Written as a function declaration
function Heading(): React.ReactNode {
  return <h1>My Website Heading</h1>
}

// Written as a function expression
const OtherHeading: React.FC = () => <h1>My Website Heading</h1>

Обратите внимание на ключевое отличие здесь. В первом примере мы пишем нашу функцию как объявление функции . Мы аннотируем возвращаемый тип , потому React.Nodeчто это то, что он возвращает. Напротив, во втором примере используется функциональное выражение. Поскольку второй экземпляр возвращает функцию, а не значение или выражение, мы аннотируем тип функции для React.FC React «Функциональный компонент».

Это может сбивать с толку, чтобы помнить два. В основном это вопрос выбора дизайна. Что бы вы ни выбрали для своего проекта, используйте его последовательно.

Props

Следующее основное понятие, которое мы рассмотрим, — props. Вы можете определить свои props, используя либо интерфейс, либо тип. Давайте посмотрим на другой пример:

import React from 'react'

interface Props {
  name: string;
  color: string;
}

type OtherProps = {
  name: string;
  color: string;
}

// Notice here we're using the function declaration with the interface Props
function Heading({ name, color }: Props): React.ReactNode {
  return <h1>My Website Heading</h1>
}

// Notice here we're using the function expression with the type OtherProps
const OtherHeading: React.FC<OtherProps> = ({ name, color }) =>
  <h1>My Website Heading</h1>

Когда дело доходит до типов или интерфейсов, мы предлагаем следовать рекомендациям, представленным сообществом react-typescript-cheatsheet:

  • «Всегда используйте интерфейс для общедоступного API-определения при создании библиотеки или сторонних определений внешнего типа».
  • «рассмотрите возможность использования типа для props и состояния вашего компонента React, потому что он более ограничен».

Вы можете прочитать больше об обсуждении и увидеть удобную таблицу сравнения типов и интерфейсов здесь .

Давайте посмотрим на еще один пример, чтобы мы могли увидеть что-то более практичное:

import React from 'react'

type Props = {
  /** color to use for the background */
  color?: string;
  /** standard children prop: accepts any valid React Node */
  children: React.ReactNode;
  /** callback function passed to the onClick handler*/
  onClick: ()  => void;
}

const Button: React.FC<Props> = ({ children, color = 'tomato', onClick }) => {
   return <button style={{ backgroundColor: color }} onClick={onClick}>{children}</button>
}

В <Button /> компоненте мы используем тип для нашего Props. Каждое свойство имеет краткое описание, указанное над ним, чтобы предоставить больше контекста другим разработчикам. После ? названия свойства color указывает, что это необязательно. Свойство children принимает a, React.ReactNode потому что оно принимает все, что является допустимым возвращаемым значением компонента.

Чтобы учесть нашу необязательную color опору, мы используем значение по умолчанию при ее деструктурировании. Этот пример должен охватывать основы и показывать, что вы должны писать типы для своих Props и использовать как необязательные значения, так и значения по умолчанию.

В общем, помните об этих вещах при написании Props в проекте React и TypeScript:

  • Всегда добавляйте описательные комментарии к prop, используя нотацию TSDoc /** comment */.
  • Независимо от того, используете ли вы типы или интерфейсы для свойств вашего компонента, используйте их последовательно.
  • Когда props являются необязательными, обрабатывайте их соответствующим образом или используйте значения по умолчанию.

Хуки

К счастью, вывод типа TypeScript хорошо работает при использовании хуков. Это означает, что вам не о чем беспокоиться. Например, возьмем этот пример:

// `value` is inferred as a string
// `setValue` is inferred as (newValue: string) => void
const [value, setValue] = useState('')

TypeScript выводит значения, данные для использования хуком useState. Это та область, где React и TypeScript просто работают вместе, и это прекрасно.

В редких случаях, когда вам нужно инициализировать хук с нулевым значением, вы можете использовать дженерик и передать объединение, чтобы правильно ввести свой хук. См. этот экземпляр:

type User = {
  email: string;
  id: string;
}

// the generic is the < >
// the union is the User | null
// together, TypeScript knows, "Ah, user can be User or null".
const [user, setUser] = useState<User | null>(null);

Другое место, где TypeScript сияет с помощью хуков, — это userReducer, где вы можете воспользоваться преимуществами размеченных объединений . Вот полезный пример:

type AppState = {};
type Action =
  | { type: "SET_ONE"; payload: string }
  | { type: "SET_TWO"; payload: number };

export function reducer(state: AppState, action: Action): AppState {
  switch (action.type) {
    case "SET_ONE":
      return {
        ...state,
        one: action.payload // `payload` is string
      };
    case "SET_TWO":
      return {
        ...state,
        two: action.payload // `payload` is number
      };
    default:
      return state;
  }
}

Красота здесь заключается в полезности разрозненных союзов. Обратите внимание Action на объединение двух похожих друг на друга объектов. Свойство typeявляется строковым литералом . Разница между этим и типом stringзаключается в том, что значение должно соответствовать литеральной строке, определенной в типе. Это означает, что ваша программа более безопасна, поскольку разработчик может вызвать только действие, для которого установлен typeключ "SET_ONE"или "SET_TWO".

Как видите, хуки не усложняют природу проекта React и TypeScript. Во всяком случае, они хорошо подходят для дуэта.

Общие случаи использования

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

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

Одним из наиболее распространенных случаев является правильный ввод 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"/>
}

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

Иногда вы хотите взять props компонента, объявленные для одного компонента, и расширить их, чтобы использовать их в другом компоненте. Но вы можете изменить один или два. Ну, помните, как мы рассматривали два способа ввода props компонентов, типов и интерфейсов? В зависимости от того, что вы использовали, определяется, как вы расширяете свойства компонента. Давайте сначала посмотрим на способ использования 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>
}

Если вы объявили свои props с помощью 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>
}

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

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

Будь то для клиента GraphQL, такого как Apollo , или для тестирования с чем-то вроде React Testing Library , мы часто используем сторонние библиотеки в проектах React и TypeScript. Когда это происходит, первое, что вы хотите сделать, это посмотреть, есть ли пакет @types с определениями типов TypeScript. Вы можете сделать это, запустив:

#yarn
yarn add @types/<package-name>

#npm
npm install @types/<package-name>

Например, если вы используете Jest , вы можете сделать это, запустив:

#yarn
yarn add @types/jest

#npm
npm install @types/jest

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

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

Должны ли они быть сохранены как dependenciesили devDependenciesв моем package.json?

Короткий ответ: «это зависимость». В большинстве случаев они могут выйти из строя, devDependenciesесли вы создаете веб-приложение. Однако, если вы пишете библиотеку React на TypeScript, вы можете включить их как файлы dependencies.

На Stack Overflow есть несколько ответов на этот вопрос , которые вы можете проверить для получения дополнительной информации.

Что произойдет, если у них нет пакета @types?

Если вы не найдете @types пакет в npm, у вас есть два варианта:

  1. Добавьте базовый файл объявления
  2. Добавить подробный файл декларации

Первый вариант означает, что вы создаете файл на основе имени пакета и кладете его в корень. Если, например, нам нужны типы для нашего пакета banana-js, то мы могли бы создать базовый файл объявления, вызываемый banana-js.d.tsв корне:

declare module 'banana-js';

Это не обеспечит вам безопасность типов, но разблокирует вас.

Более подробный файл объявления будет там, где вы добавляете типы для библиотеки/пакета:

declare namespace bananaJs {
    function getBanana(): string;
    function addBanana(n: number) void;
    function removeBanana(n: number) void;
}

Вывод

Лучшее совместное использование React и TypeScript требует некоторого обучения из-за большого количества информации, но в долгосрочной перспективе преимущества окупаются. В этой статье мы рассмотрели конфигурацию, компоненты, Props, хуки, распространенные варианты использования и сторонние библиотеки.