React і TypeScript — надзвичайно популярні технології серед розробників. Часто опанувати їхні особливості непросто, а правильне рішення зовсім не лежить на поверхні. Тому ми зібрали найкращі практики з прикладами, аби прояснити концепції спільної роботи технологій.
Поглянемо ближче!
Як React та TypeScript працюють разом?
Перш ніж почнемо, пригадаємо, як React та TypeScript працюють разом. React — «JavaScript-бібліотека для створення користувацьких інтерфейсів», водночас TypeScript — це «типізована надбудова над JavaScript, яка компілюється в нього». Використовуючи обидві технології в тандемі, ми створюємо UI за допомогою типізованого JavaScript.
Варто застосовувати згадані технології разом, аби отримати переваги статично типізованої мови (TypeScript) при створенні інтерфейсу.
TypeScript компілює мій React-код?
Досить часто це питання цікавить розробників. Насправді весь процес відбувається подібно до діалогу:
TS: «Привіт! Це твій UI-код?»
React: «Ага».
TS: «Круто! Я скомпілюю його та переконаюсь, чи не забув ти про щось».
React: «Звучить класно!»
Тобто відповідь: так, однозначно! Але пізніше, коли ми торкнемося tsconfig.json
, більшість часу вам захочеться використовувати "noEmit": true
. Це означає, що TypeScript не буде генерувати JavaScript після компіляції. Все тому, що ми просто беремо TypeScript для перевірки типів.
Результат обробляється в налаштуваннях create-react-app
за допомогою react-scripts
. Ми запускаємо yarn build
— і скрипти React збирають наш проєкт для продакшена.
Отже, TypeScript компілює React-код, аби провести перевірку типів. Він не генерує вихідний JavaScript (у більшості випадків). Результат і далі має вигляд звичайного React-проєкту.
Чи може TypeScript працювати з React і Webpack?
Так, TypeScript працює з React і Webpack. На щастя для вас, в офіційному посібнику з TypeScript є інформація про налаштування.
Нарешті, освіживши трохи знання про взаємодію React і TypeScript, перейдемо до найкращих практик.
Найкращі практики
Автори матеріалу дослідили найпоширеніші запитання та створили перелік найбільш корисних та популярних варіантів використання React і TypeScript. З ними ваші проєкти будуть ще досконалішими.
Конфігурація
Найважливіша, проте не найцікавіша, частина розробки — це налаштування. Як нам швидко налаштувати все необхідне і отримати продуктивний та ефективний проєкт? Розглянемо налаштування проєкту, що містить:
-
tsconfig.json
; - ESLint;
- Prettier;
- VS Code extensions and settings.
Перейдемо до налаштувнаня проєкту.
Найшвидший спосіб почати роботу з 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", // Визначаємо цільову версію ECMAScript
"lib": [
"dom",
"dom.iterable",
"esnext"
], // Перелік бібліотек, які будуть додані до компіляції
"allowJs": true, // дозволяємо компіляцію JavaScript-файлів
"skipLibCheck": true, //пропускаємо перевірку типів для бібліотечних файлів
"esModuleInterop": true, // замінюємо імпорти простору імен (import * as fs from "fs") на імпорти CJS/AMD/UMD (import fs from "fs")
"allowSyntheticDefaultImports": true, // Дозволяємо імпорти за замовчуванням з модулів без експорту за замовчуванням
"strict": true, // Дозволяємо всі суворі перевірки типів
"forceConsistentCasingInFileNames": true, // Забороняємо непослідовні посилання на той самий файл
"module": "esnext", // визначаємо генерацію коду модуля
"moduleResolution": "node", // Визначаємо модулі, використовуючи стиль Node.js
"resolveJsonModule": true, // Включаємо імпортовані модулі з розширенням .json
"noEmit": true, // Не генеруємо результат (тобто не компілюємо код, просто перевіряємо типи)
"jsx": "react" // Підтримуємо JSX у .tsx-файлах
"sourceMap": true, // *** Генеруємо відповідний .map-файл ***
"declaration": true, // *** Генеруємо відповідний .d.ts-файл ***
"noUnusedLocals": true, // *** Повідомляємо про помилки через невикористані локальні змінні ***
"noUnusedParameters": true, // *** Повідомляємо про помилки через невикористані параметри ***
"experimentalDecorators": true // *** Активуємо експериментальну підтримку для ES-декораторів ***
"incremental": true // *** Активуємо поступову компіляцію шляхом зчитування/запису інформації з попередніх компіляцій у файл/на диск ***
"noFallthroughCasesInSwitch": true // *** Повідомляємо про помилки через Report errors for невдалі case-вирази у switch ***
},
"include": [
"src/**/*" // *** файли, які потребують перевірки типів TypeScript ***
],
"exclude": ["node_modules", "build"] // *** файли, які не потрібно перевіряти ***
}
Додаткові рекомендації отримано від спільноти react-typescript-cheatsheet
та з офіційної документації компіляції. Це чудовий ресурс, якщо ви хочете дізнатись більше про інші налаштування та їх призначення.
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/explicit-function-return-type": "off",
},
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 та показуємо помилки prettier у вигляді помилок ESLint. Переконайтеся, що це завжди остання конфігурація в масиві extends.
],
parserOptions: {
ecmaVersion: 2018, // Дозволяємо обробку сучасних фіч ECMAScript
sourceType: 'module', // Дозволяємо використання імпортів
ecmaFeatures: {
jsx: true, // Дозволяємо обробку JSX
},
},
rules: {
// Тут ми визначаємо правила ESLint. Також можна перевизначати правила наявних конфігів
// наприклад, "@typescript-eslint/explicit-function-return-type": "off",
},
settings: {
react: {
version: 'detect', // Вказуємо eslint-plugin-react автоматично визначати версію React для використання
},
},
};
Рекомендації отримано з ком'юніті-ресурсу, вони називаються «Використання ESLint та Prettier у TypeScript-проєкті».
Розширення та налаштування VSCode
Ми вже додали ESLint та Prettier, далі налаштуємо розширення нашого редактора коду, щоб він автоматично форматував його при збереженні.
Спершу завантажимо ESLint-розширення для VSCode. Так ми зможемо плавно інтегрувати ESLint в редактор.
Далі перейдемо до налаштування робочого середовища, додавши такі конфіги до файлу .vscode/settings.json
:
{
"eslint.autoFixOnSave": true,
"eslint.validate": [
"javascript",
"javascriptreact",
{ "language": "typescript", "autoFix": true },
{ "language": "typescriptreact", "autoFix": true }
],
"editor.formatOnSave": true,
"[javascript]": {
"editor.formatOnSave": false
},
"[javascriptreact]": {
"editor.formatOnSave": false
},
"[typescript]": {
"editor.formatOnSave": false
},
"[typescriptreact]": {
"editor.formatOnSave": false
}
}
Тепер VS Code стане вашим помічником на шляху до бездоганного коду.
Описані рекомендації було запозичено з попереднього матеріалу «Використання ESLint та Prettier у TypeScript-проєкті».
Компоненти
Одна з ключових концепцій React — компоненти. Тут ми посилатимемось на стандартні компоненти React v16.8, тобто ті, що використовують хуки, на відміну від компонентів-класів.
Варто подбати про багато нюансів, використовуючи базові компоненти. Розглянемо приклад:
import React from 'react'
// Компонент у вигляді function declaration
function Heading(): React.ReactNode {
return <h1>My Website Heading</h1>
}
// Компоненти у вигляді функціонального виразу
const OtherHeading: React.FC = () => <h1>My Website Heading</h1>
Зверніть увагу на ключові відмінності у наведених фрагментах. Перший фрагмент демонструє функціональне оголошення. Ми позначаємо значення, що повертається як React.Node
. Натомість другий приклад демонструє функціональний вираз. Оскільки поверненим значенням буде функція, ми позначаємо тип як React.FC
.
Спочатку може бути складно запам'ятати обидва варіанти. Все залежить від обраного підходу до проєктування. Та який би варіант ви не обрали, використовуйте його постійно.
Props
Наступна наша ключова концепція — props
. Ви можете оголосити власні пропси, використовуючи інтерфейс чи тип. Розглянемо приклад:
import React from 'react'
interface Props {
name: string;
color: string;
}
type OtherProps = {
name: string;
color: string;
}
// Зверніть увагу, що ми використовуємо оголошення функції з інтерфейсом Props
function Heading({ name, color }: Props): React.ReactNode {
return <h1>My Website Heading</h1>
}
// Зверніть увагу, що тут ми використовуємо функціональний вираз з типом OtherProps
const OtherHeading: React.FC<OtherProps> = ({ name, color }) =>
<h1>My Website Heading</h1>
Коли справа доходить до типів чи інтерфейсів, автор пропонує дослухатись до кількох порад спільноти react-typescript-cheatsheet
:
- завжди використовуйте інтерфейси для публічних API під час створення бібліотек або визначень сторонніх типів зовнішніх середовищ;
- застосовуйте типи для пропсів вашого React-компоненту та стану.
Ви можете дізнатись більше порад та розглянути відмінності між типом та інтерфейсом за посиланням.
Розглянемо ще один, більш практичний приклад:
import React from 'react'
type Props = {
/** колір для фону */
color?: string;
/** стандартний дочірній проп: приймає будь-який валідний вузол React */
children: React.ReactNode;
/** колбек, переданий обробнику onClick */
onClick: () => void;
}
const Button: React.FC<Props> = ({ children, color = 'tomato', onClick }) => {
return <button style={{ backgroundColor: color }} onClick={onClick}>{children}</button>
}
В компоненті <Button />
ми використали тип для наших props. Кожен проп містить коротке описання для розуміння іншими розробниками. Знак ?
після color
означає, що параметр необов'язковий.
Параметр children
приймає тип React.ReactNode
, тобто будь-яке валідне значення, повернене компонентом (більше за посиланням).
Враховуючи необов'язковість нашого параметра color
, передаємо автоматичне значення при деструктуризації.
В прикладі вище ми розглянули базові концепції та продемонстрували, що необхідно використовувати типи для пропсів, а також опціональні та автоматичні параметри разом.
Слід пам'ятати про деякі моменти при використанні props у проєкті на React і TypeScript:
- завжди додавайте зрозумілі коментарі для ваших props, використовуючи нотацію
/** comment */
; - незалежно від того, використовуєте ви типи чи інтерфейси для props ваших компонентів, дотримуйтесь постійності;
- якщо параметр необов'язковий, не забувайте обробити його відсутність чи використати автоматичне значення.
Хуки
На щастя, інтерфейси TypeScript також добре працюють з хуками. Розглянемо приклад:
// тип `value` витікає з типу автоматичного значення
// тип `setValue` визначається як (newValue: string) => void
const [value, setValue] = useState('')
TypeScript автоматично визначає тип значення, переданого до хука useState
. Це той випадок, коли React і TypeScript чудово співпрацюють.
У рідкісних випадках, коли вам необхідно ініціалізувати хук null
, ви можете використати дженерик та передати туди перелік допустимих типів хука. Одразу приклад:
type User = {
email: string;
id: string;
}
// дженериком називається < >
// об'єднанням називається User | null
// тобто TypeScript розуміє, що змінна user може бути типу User чи 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` типу рядок
};
case "SET_TWO":
return {
...state,
two: action.payload // `payload` типу число
};
default:
return state;
}
}
Джерело: react-typescript-cheatsheet
розділ Хуків
Уся краса тут в корисності розмічених об'єднань. Зверніть увагу, Action
визначається як об'єднання двох схожих об'єктів. Властивість type
— рядковий літерал. Відмінність від типу string
у тому, що значення повинно відповідати літералу рядка, визначеного в типі. Тобто ваш застосунок буде супербезпечним, адже розробник може лише викликати action
з параметром type
зі значенням "SET_ONE"
або "SET_TWO"
.
Як бачимо, хуки не додають складності в розробці React/TypeScript-проєктів, а навпаки добре працюють в тандемі.
Поширені варіанти використання
Далі поговоримо про найбільш поширені задачі, що спричиняють труднощі у розробників, коли вони використовують TypeScript разом з React. Сподіваємось, що інформація далі дозволить вам оминати ці перешкоди.
Обробка подій форми
Одна з найбільш поширених задач — коректне використання обробника onChange
при зміні поля форми. Розглянемо приклад:
import React from 'react'
const MyInput = () => {
const [value, setValue] = React.useState('')
// Тип події – "ChangeEvent"
// В дженерик ми передаємо тип "HTMLInputElement"
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
setValue(e.target.value)
}
return <input value={value} onChange={onChange} id="input-example"/>
}
Розширення props-компонента
Іноді ви хочете взяти вже наявні пропси одного компонента та розширити їх, аби використовувати в іншому компоненті. Але при цьому вам треба змінити деякі з них.
Добре, пригадаймо, як ми розглядали два способи типізувати параметри компонента: користувацькі типи та інтерфейси. Спосіб типізації визначає механізм розширення параметрів. Спочатку з'ясуємо, як працювати з type
:
import React from 'react';
type ButtonProps = {
/** фоновий колір кнопки */
color: string;
/** текст всередині кнопки */
text: string;
}
type ContainerProps = ButtonProps & {
/** висота контейнера (значення використовується в пікселях) */
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 {
/** фоновий колір кнопки */
color: string;
/** текст всередині кнопки */
text: string;
}
interface ContainerProps extends ButtonProps {
/** висота контейнера (значення використовується в пікселях) */
height: number;
}
const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {
return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>
}
Обидва методи розв'язують ту саму проблему. Вам вирішувати, що краще пасуватиме до проєкту. На думку автора, розширення інтерфейсу видається більш читабельним, але вирішувати вам і команді.
Ви можете заглибитись в обидві концепції за посиланнями:
Сторонні бібліотеки
Неважливо, чи ми використовуємо сторонні бібліотеки у проєктах на React і TypeScript для GraphQL-клієнта на зразок Apollo, чи для тестування з React Testing Library. Коли справа доходить до сторонніх бібліотек, перше, на що варто звернути увагу, — пакет @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
Так ви точно будете спокійними щодо типів при використанні бібліотеки у своєму проєкті.
Простір імен @types
зарезервовано для оголошень типу пакета. Вони розташовані в репозиторії DefinitelyTyped, який частково підтримується спільнотою TypeScript, а частково — ком'юніті.
Зберігати типи треба як dependencies
чи devDependencies
в package.json
?
Коротка відповідь: все залежить від потреб. У більшості випадків краще обрати devDependencies
, якщо ви створюєте веб-застосунок. Однак вам можуть знадобитися dependencies
,якщо ви пишете React-бібліотеку на TypeScript .
За детальною інформацією зверніться до відповідей на Stack Overflow.
Що робити, якщо немає пакета @types
?
Якщо ви не знайшли @types
на npm, у вас є такі варіанти:
- Додати базовий файл оголошень;
- Додати заглиблений файл оголошень.
Перший варіант означає, що ви створюєте файл, який базується на назві пакета, та розміщуєте його в корені. Якщо, наприклад, вам потрібні типи для пакета 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;
}
Якщо у вас немає досвіду створення файлів оголошень, подивіться офіційний посібник TypeScript з цієї теми.
Що далі?
Якщо ви б хотіли заглибитись у вивчення технології, можете ознайомитись з додатковими матеріалами:
- Шпаргалки з React-TypeScript— якщо шукаєте особливі приклади та деталі.
- Офіційний посібник з TypeScript — підтримується командою TypeScript і пропонує приклади та докладні пояснення принципів роботи мови.
- TypeScript Playground — тестуйте код на React з TypeScript одразу в браузері.
Ще немає коментарів