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

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

Установка

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

Додавання TypeScript до існуючого React-проекту

Як встановити останню версію визначень типів React:

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

.

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

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

Типізація сценарію за допомогою React-компонентів

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

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

Взявши MyButton компонент з посібника Швидкий старт, ми можемо додати тип, що описує заголовок для кнопки:

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

export default function MyApp() {
  return (
    <div>
      <h1>Welcome to my app</h1>
      <MyButton title="I'm a button" />
    </div>
  );
}
import AppTSX from "./App.tsx";
export default App = AppTSX;

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

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

interface MyButtonProps {
  /** The text to display inside the button */
  title: string;
  /** Whether the button can be interacted with */
  disabled: boolean;
}

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

export default function MyApp() {
  return (
    <div>
      <h1>Welcome to my app</h1>
      <MyButton title="I'm a disabled button" disabled={true}/>
    </div>
  );
}
import AppTSX from "./App.tsx";
export default App = AppTSX;

Тип, що описує пропси вашого компонента, може бути настільки простим або складним, наскільки вам потрібно, хоча це має бути об'єктний тип, описаний за допомогою type або interface. Ви можете дізнатися про те, як TypeScript описує об'єкти у Типи об'єктів, але вас також може зацікавити використання Типів об'єднань для опису реквізиту, який може бути одним з декількох різних типів, а також посібник Створення типів з типів для більш складних випадків використання.

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

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

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

useState

Хук useState повторно використає значення, передане в якості початкового стану, щоб визначити тип значення. Наприклад:

// Infer the type as "boolean"
const [enabled, setEnabled] = useState(false);

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

// Explicitly set the type to "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("Unknown action");
  }
}

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>Welcome to my counter</h1>

      <p>Count: {state.count}</p>
      <button onClick={addFive}>Add 5</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}
import AppTSX from "./App.tsx";
export default App = AppTSX;

Ми використовуємо 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>Current theme: {theme}</p>
    </div>
  )
}
import AppTSX from "./App.tsx";
export default App = AppTSX;

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

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

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

// This is a simpler example, but you can imagine a more complex object here
type ComplexObject = {
  kind: string
};

// The context is created with `| null` in the type, to accurately reflect the default value.
const Context = createContext<ComplexObject | null>(null);

// The `| null` will be removed via the check in the Hook.
const useGetComplexObject = () => {
  const object = useContext(Context);
  if (!object) { throw new Error("useGetComplexObject must be used within a Provider") }
  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>Current object: {object.kind}</p>
    </div>
  )
}

useMemo

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

// The type of visibleTodos is inferred from the return value of filterTodos
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);

useCallback

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

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

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

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

import { useState, useCallback } from 'react';

export default function Form() {
  const [value, setValue] = useState("Change me");

  const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>((event) => {
    setValue(event.currentTarget.value);
  }, [setValue])
  
  return (
    <>
      <input value={value} onChange={handleChange} />
      <p>Value: {value}</p>
    </>
  );
}

Корисні типи

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

Події DOM

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

import { useState } from 'react';

export default function Form() {
  const [value, setValue] = useState("Change me");

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

  return (
    <>
      <input value={value} onChange={handleChange} />
      <p>Value: {value}</p>
    </>
  );
}
import AppTSX from "./App.tsx";
export default App = AppTSX;

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

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

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

Діти

Існує два поширених способи опису дочірніх компонентів. Перший полягає у використанні типу 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-властивостей і є гарним способом переконатися, що ви передаєте правильні CSS-властивості до пропу style, а також отримати автозаповнення у вашому редакторі.

interface MyComponentProps {
  style: React.CSSProperties;
}

Подальше навчання

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

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

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

  • У примітках до випуску TypeScript детально описано кожну нову можливість.

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

  • TypeScript Community Discord - чудове місце, де можна поставити запитання та отримати допомогу з проблемами TypeScript та React.