⚛️ 12 советов по внедрению TypeScript в React-приложениях
React и TypeScript – две потрясающие технологии, используемые в наши дни множеством разработчиков. В этой статье мы постарались собрать лучшие практики внедрения TypeScript в React-приложениях.
Зачем использовать TypeScript в React?
React – это JavaScript-библиотека с открытым исходным кодом для создания пользовательских интерфейсов, а TypeScript – типизированное надмножество JavaScript. Объединяя их, мы создаем пользовательские интерфейсы, используя типизированную версию JavaScript. Это означает большую безопасность и меньшее количество ошибок, отправляемых во внешний интерфейс.
Не существует единственного правильного способа написания кода React с использованием TypeScript. Как и в случае с другими технологиями, если ваш код компилируется и работает, вы, вероятно, что-то сделали правильно.
Подготовка компонентов к совместному использованию с помощью TypeScript
Bit.dev стал популярной альтернативой традиционным библиотекам компонентов, поскольку он предлагает способ собрать и поделиться отдельными компонентами из любой кодовой базы.
Создавая проекты с использованием React с TS, вы убедитесь, что ваши компоненты легко понятны другим разработчикам. Это отличный способ написать поддерживаемый код и оптимизировать совместную работу команды.
Настройка проекта и tsconfig.json
Самый быстрый способ запустить приложение React/TypeScript – использовать create-react-app
с шаблоном TypeScript:
npx create-react-app my-app --template typescript
tsconfig.json
это файл конфигурации TypeScript. Файл содержит первоначальные настройки, ниже приведем несколько параметров с пояснениями:
{ "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 в свой проект. Эти файлы содержат определения типов и интерфейсов: чтобы понять свойства или доступность конкретного типа, вы можете открыть эти файлы и просмотреть их содержимое.
Там вы увидете небольшой раздел файла index.d.ts
, показывающий подписи для функции createElement
.
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', // Указывает парсер 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
в корне и добавьте в него следующий код:
module.exports = { semi: true, trailingComma: 'all', singleQuote: true, printWidth: 120, tabWidth: 4, };
- Обновите файл
.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
:
{ "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 или в публикациях «Библиотеки программиста».