Використання TypeScript

TypeScript — це популярний спосіб додавання визначень типів (type definitions) до вихідного коду JavaScript. TypeScript підтримує JSX без додаткових налаштувань, і ви можете отримати повну підтримку React Web, встановивши пакети @types/react і @types/react-dom у своєму проєкті.

Встановлення

Усі готові для впровадження React-фреймворки підтримують використання TypeScript. Дотримуйтесь інструкції для встановлення у відповідному фреймворку:

Додавання TypeScript до наявного React-проєкту

Щоб встановити останню версію визначень типів для React, виконайте команду:

Terminal
npm install @types/react @types/react-dom

У вашому tsconfig.json потрібно вказати такі параметри компілятора:

  1. dom має бути додано до параметру lib (Зауважте: якщо параметр lib не вказаний, то dom вже додано).
  2. Параметр jsx повинен мати одне з допустимих значень. Для більшості застосунків достатньо вказати preserve. Якщо ви публікуєте бібліотеку, зверніться до документації jsx, щоб вибрати правильне значення.

TypeScript з компонентами React

Note

Кожен файл, що містить JSX, повинен використовувати розширення файлу .tsx. Це специфічне для TypeScript розширення, яке повідомляє TypeScript, що цей файл містить JSX.

Написання TypeScript коду з React дуже схоже на написання JavaScript коду з React. Основна різниця під час роботи з компонентом полягає в тому, що ви можете вказувати типи пропсів вашого компонента. Ці типи можна використовувати для перевірки правильності коду та надання вбудованої документації в редакторах коду.

Додамо тип для пропу title у кнопці, що є компонентом MyButton з розділу “Швидкий старт”:

function MyButton({ title }: { title: string }) {
  return (
    <button>{title}</button>
  );
}

export default function MyApp() {
  return (
    <div>
      <h1>Ласкаво прошу до мого застосунку</h1>
      <MyButton title="Кнопка" />
    </div>
  );
}

Note

Ці пісочниці можуть обробляти код TypeScript, але вони не виконують перевірку типів. Це означає, що ви можете вносити зміни до пісочниці TypeScript для навчання, але не отримаєте жодних помилок або попереджень щодо типів. Для цього краще використовувати пісочницю TypeScript або іншу онлайн-пісочницю з більшим функціоналом.

Цей вбудований синтаксис є найпростішим способом надати типи для компонента, хоча для більшої кількості полів це може бути незручним. Тоді ви можете використовувати interface або type для опису пропсів компонента:

interface MyButtonProps {
  /** Текст для відображення всередині кнопки */
  title: string;
  /** Чи можна взаємодіяти з кнопкою */
  disabled: boolean;
}

function MyButton({ title, disabled }: MyButtonProps) {
  return (
    <button disabled={disabled}>{title}</button>
  );
}

export default function MyApp() {
  return (
    <div>
      <h1>Ласкаво прошу до мого застосунку</h1>
      <MyButton title="Неактивна кнопка" disabled={true}/>
    </div>
  );
}

Тип, що описує пропси вашого компонента, може бути настільки простим або складним, наскільки ви забажаєте, хоч вони і повинні бути об’єктом, описані за допомогою type або interface. Дізнайтеся про те, як TypeScript описує об’єкти, у статті “Об’єктні типи”; ви також можете бути зацікавлені у використанні типів об’єднання (union types) для опису пропу з кількома різними типами або у створенні типів із типів для інших більш складних випадків.

Приклади для хуків

Визначення типів з пакету @types/react включають типи вбудованих хуків для використання у компонентах без додаткових налаштувань. Вони створені з урахуванням коду у вашому компоненті, тому ви часто отримуватиме виведені типи (inferred types) і, в ідеалі, не матимете потреби розбиратися з дрібницями надання типів.

Розглянемо кілька прикладів того, як вказати типи для хуків.

useState

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

// Виведення типу як "boolean"
const [enabled, setEnabled] = useState(false);

У цьому прикладі тип boolean буде заданий для змінної enabled, а setEnabled буде функцією, яка приймає або аргумент типу boolean, або функцію, що повертає boolean. Якщо ви хочете явно вказати тип для стану, передайте аргумент типу у виклику useState:

// Явно задати тип "boolean"
const [enabled, setEnabled] = useState<boolean>(false);

Це не дуже корисно у попередньому випадку, але зазвичай ви захочете явно вказати тип, коли у вас є тип об’єднання. Наприклад, status тут може бути лише однією з кількох різних стрічкових змінних:

type Status = "idle" | "loading" | "success" | "error";

const [status, setStatus] = useState<Status>("idle");

Або, як рекомендується у принципах структурування стану, ви можете згрупувати відповідний стан в об’єкт та описати різні варіанти через об’єктні типи:

type RequestState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success', data: any }
| { status: 'error', error: Error };

const [requestState, setRequestState] = useState<RequestState>({ status: 'idle' });

useReducer

Хук useReducer є більш складним, адже приймає функцію-редюсер та початковий стан. Типи для функції-редюсера виводяться з початкового стану. Щоб описати тип для стану, ви можете за бажанням передати аргумент типу у виклику useReducer, але натомість краще вказати тип для початкового стану:

import {useReducer} from 'react';

interface State {
   count: number 
};

type CounterAction =
  | { type: "reset" }
  | { type: "setCount"; value: State["count"] }

const initialState: State = { count: 0 };

function stateReducer(state: State, action: CounterAction): State {
  switch (action.type) {
    case "reset":
      return initialState;
    case "setCount":
      return { ...state, count: action.value };
    default:
      throw new Error("Невідома дія");
  }
}

export default function App() {
  const [state, dispatch] = useReducer(stateReducer, initialState);

  const addFive = () => dispatch({ type: "setCount", value: state.count + 5 });
  const reset = () => dispatch({ type: "reset" });

  return (
    <div>
      <h1>Ласкаво прошу до мого лічильника</h1>

      <p>Сума: {state.count}</p>
      <button onClick={addFive}>Додати 5</button>
      <button onClick={reset}>Скинути</button>
    </div>
  );
}

Ми використовуємо TypeScript у кількох основних місцях:

  • interface State описує структуру стану редюсера.
  • type CounterAction описує різні дії, які можуть бути відправлені в редюсер.
  • const initialState: State задає тип для початкового стану, а також тип, який стандартно використовується у useReducer.
  • stateReducer(state: State, action: CounterAction): State задає типи для аргументів функції-редюсера та значення, яке вона повертає.

Більш явною альтернативою заданню типу для initialState є передача аргументу типу в useReducer:

import { stateReducer, State } from './your-reducer-implementation';

const initialState = { count: 0 };

export default function App() {
const [state, dispatch] = useReducer<State>(stateReducer, initialState);
}

useContext

Хук useContext — це техніка передачі даних деревом компонентів без необхідності передавати пропси через компоненти. Цей хук використовується шляхом створення компоненту-провайдера та хука для отримання значення у дочірньому компоненті.

Тип значення, яке надається контекстом, виводиться зі значення, переданого до createContext:

import { createContext, useContext, useState } from 'react';

type Theme = "light" | "dark" | "system";
const ThemeContext = createContext<Theme>("system");

const useGetTheme = () => useContext(ThemeContext);

export default function MyApp() {
  const [theme, setTheme] = useState<Theme>('light');

  return (
    <ThemeContext.Provider value={theme}>
      <MyComponent />
    </ThemeContext.Provider>
  )
}

function MyComponent() {
  const theme = useGetTheme();

  return (
    <div>
      <p>Поточна тема: {theme}</p>
    </div>
  )
}

Ця техніка спрацьовує, коли у вас є початкове значення з певним змістом, але іноді його немає, і тоді null може здатися прийнятним початковим значенням. Однак, щоб дозволити системі типізації розуміти ваш код, вам потрібно явно задати ContextShape | null для createContext.

Це спричиняє необхідність усунення | null з типу для споживачів контексту. Ми рекомендуємо, щоб хук здійснював перевірку під час виконання щодо існування значення та викидав помилку, якщо воно відсутнє:

import { createContext, useContext, useState, useMemo } from 'react';

// Це простіший приклад, але ви можете уявити тут більш складний об'єкт.
type ComplexObject = {
kind: string
};

// Контекст створюється з `| null` у типі, щоб точно відображати початкове значення.
const Context = createContext<ComplexObject | null>(null);

// `| null` буде видалено через перевірку у хуку.
const useGetComplexObject = () => {
const object = useContext(Context);
if (!object) { throw new Error("useGetComplexObject має використовуватись всередині компонента-провайдера") }
return object;
}

export default function MyApp() {
const object = useMemo(() => ({ kind: "complex" }), []);

return (
<Context.Provider value={object}>
<MyComponent />
</Context.Provider>
)
}

function MyComponent() {
const object = useGetComplexObject();

return (
<div>
<p>Поточний об'єкт: {object.kind}</p>
</div>
)
}

useMemo

Хуки useMemo створять/повторно отримають доступ до збереженого значення після виклику функції і повторно викличуть функцію лише тоді, коли зміняться його залежності, що передані як другий параметр. Результат виклику хука виводиться зі значення, яке повертає функція у першому параметрі. Але також можна явно передати аргумент типу хуку.

// Тип змінної visibleTodos виведений зі значення, поверненого з функції filterTodos
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);

useCallback

Хук useCallback надає однакове посилання на функцію, допоки залежності у другому параметрі залишаються тими ж. Як і в useMemo, тип функції виводиться зі значення, що повертається функцією у першому параметрі, і також можна явно передати аргумент типу хуку.

const handleClick = useCallback(() => {
// ...
}, [todos]);

Під час роботи TypeScript у строгому режимі (strict mode) useCallback вимагає додавання типів для параметрів функції зворотного виклику. Це тому, що тип функції зворотного виклику виводиться зі значення, поверненого з функції, і без знання параметрів його не можливо точно визначити.

Залежно від уподобань щодо стилю коду ви можете використовувати функції *EventHandler з типами React, щоб надати тип обробнику подій одночасно з визначенням функції зворотного виклику:

import { useState, useCallback } from 'react';

export default function Form() {
const [value, setValue] = useState("Зміни мене");

const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>((event) => {
setValue(event.currentTarget.value);
}, [setValue])

return (
<>
<input value={value} onChange={handleChange} />
<p>Значення: {value}</p>
</>
);
}

Корисні типи

У пакеті @types/react є досить значний набір типів, який варто переглянути, коли ви відчуєте себе впевнено у роботі із взаємодією React і TypeScript. Ви можете знайти їх у каталозі React з репозиторію DefinitelyTyped. Тут ми розглянемо декілька найбільш поширених типів.

Події DOM

Працюючи з подіями DOM у React, часто можна вивести тип події з її обробника. Однак, коли ви хочете винести функцію, щоб передати її обробнику подій, вам потрібно явно вказати тип події.

import { useState } from 'react';

export default function Form() {
  const [value, setValue] = useState("Зміни мене");

  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    setValue(event.currentTarget.value);
  }

  return (
    <>
      <input value={value} onChange={handleChange} />
      <p>Значення: {value}</p>
    </>
  );
}

У типах React є багато типів подій — тут можна знайти повний список, який оснований на найпопулярніших подіях у DOM.

Щоб знайти потрібний тип, спочатку можна переглянути інформацію, яка з’являється під час наведення курсору на певний обробник і показує його тип події.

Якщо вам потрібна подія, яка не включена у цей список, ви можете використовувати тип React.SyntheticEvent, який є базовим для всіх подій.

Проп children

Існують два поширені способи опису дочірніх елементів компонента. Перший — використання типу React.ReactNode, який є об’єднанням усіх можливих типів, що можуть передаватися всередину JSX-тегу:

interface ModalRendererProps {
title: string;
children: React.ReactNode;
}

Це дуже широке визначення дочірніх елементів. Другий спосіб — використання типу React.ReactElement, який охоплює лише JSX-елементи, а не JavaScript-примітиви, як-от стрічки або числа:

interface ModalRendererProps {
title: string;
children: React.ReactElement;
}

Важливо зазначити, що ви не можете використовувати TypeScript для опису того, що дочірні елементи є певним типом JSX-елементів, тому ви не можете використовувати систему типів для опису компонента, який приймає лише елементи <li>.

Ви можете побачити приклад із використанням React.ReactNode і React.ReactElement та перевіркою типів у цій пісочниці TypeScript.

Пропси стилів

Для вбудованих стилів у React ви можете використовувати React.CSSProperties для опису об’єкта, який передається у проп style. Цей тип є об’єднанням усіх можливих властивостей CSS, тож можна переконатися, що ви передаєте правильні стилі, а також мати автозаповнення у вашому редакторі коду.

interface MyComponentProps {
style: React.CSSProperties;
}

Де дізнатись більше

Цей розділ охоплює основи використання TypeScript у React, але є багато іншого, що варто знати. Окремі сторінки API в документації містять більш детальну інформацію про те, як їх використовувати у TypeScript.

Ми рекомендуємо такі ресурси:

  • Посібник TypeScript є офіційною документацією для TypeScript і охоплює більшість ключових особливостей мови.

  • Список змін TypeScript описує нові функції більш детально.

  • Шпаргалка TypeScript для React — це підтримувана спільнотою шпаргалка для використання TypeScript у React, що охоплює багато корисних прикладів та надає більше інформації, ніж цей документ.

  • Спільнота TypeScript у Discord — чудове місце, щоб задати питання та отримати допомогу у вирішенні проблем з TypeScript і React.