Как использовать TypeScript с React

В этой статье вы узнаете, как использовать TypeScript с React. К концу, у вас будет прочное понимание того, как писать код React с использованием TypeScript. Хотите посмотреть видео-версию этого руководства? Вы можете посмотреть видео ниже Содержание *

В этой статье вы узнаете, как использовать TypeScript с React.

К концу вы получите прочное понимание того, как писать код React с использованием TypeScript.

Хотите посмотреть видео-версию этого учебника? Вы можете посмотреть видео ниже:

Содержание

Предварительные требования

Для работы с этим учебником вам понадобится:

  • базовое знание работы с React
  • базовое понимание написания кода на TypeScript

Начало работы

Для начала работы с TypeScript вам нужно установить TypeScript на вашу машину. Вы можете сделать это, выполнив команду npm install -g typescript в терминале или командной строке.

Теперь мы создадим проект Vite с использованием TypeScript.

npm create vite

После выполнения команды вам будет задано несколько вопросов.

Для имени проекта введите react-typescript-demo.

Для фреймворка выберите React, а для варианта выберите TypeScript.

1_create_project
Создание проекта с использованием Vite

После создания проекта откройте его в VS Code и выполните следующие команды в терминале:

cd react-typescript-demonpm install

Теперь давайте проведем некоторую очистку кода.

Удалите файл src/App.css и замените содержимое файла src/App.tsx следующим содержимым:

const App = () => {  return <div>App</div>;};export default App;

После сохранения файла вы можете увидеть красные подчеркивания в файле, как показано ниже:

2_red_error
Ошибка версии TypeScript

Если вы получаете эту ошибку, просто нажмите Cmd + Shift + P(Mac) или Ctrl + Shift + P(Windows/Linux) для открытия панели команд VS Code и введите текст TypeScript в поле поиска и выберите опцию TypeScript: Select TypeScript Version...:

3_version_options
Варианты палитры команд VSCode

После выбора вы увидите варианты выбора между версией VS Code и версией рабочего пространства, как показано ниже:

4_select_option
Выберите версию рабочего пространства

Из этих вариантов вам необходимо выбрать вариант Use Workspace Version. После выбора этого варианта ошибка из файла App.tsx исчезнет.

Теперь откройте файл src/index.css и замените его содержимое следующим кодом:

:root {  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;  line-height: 1.5;  font-weight: 400;  font-synthesis: none;  text-rendering: optimizeLegibility;  -webkit-font-smoothing: antialiased;  -moz-osx-font-smoothing: grayscale;  -webkit-text-size-adjust: 100%;}

Теперь давайте запустим приложение, выполнив команду npm run dev.

5_app_started
Приложение запущено

Теперь щелкните по отображаемому URL и получите доступ к приложению. Вы увидите следующий начальный экран с текстом App, отображаемым в браузере.

701358b4-4bdc-49de-b008-245ef71fc929
Запущенное приложение

Основы React и TypeScript

При использовании React с TypeScript первое, что вам следует знать, это расширение файла.

Каждому файлу React + TypeScript необходимо добавить расширение .tsx.

Если файл не содержит никакого специфичного для JSX кода, то вместо расширения .tsx можно использовать расширение .ts.

Чтобы создать компонент в React с TypeScript, вы можете использовать тип FC из пакета react и использовать его после имени компонента.

Так что откройте файл src/App.tsx и замените его следующим содержимым:

import { FC } from 'react';const App: FC = () => {  return <div>App</div>;};export default App;

Теперь передадим некоторые свойства в этот компонент App.

Откройте файл src/main.tsx и передайте свойство title компоненту App следующим образом:

import React from 'react';import ReactDOM from 'react-dom/client';import App from './App.tsx';import './index.css';ReactDOM.createRoot(document.getElementById('root')!).render(  <React.StrictMode>    <App title='TypeScript Demo' />  </React.StrictMode>);

Однако при добавлении свойства title у нас теперь возникла ошибка TypeScript, как вы видите ниже:

7_prop_error
Ошибка свойства

Три способа определения типов свойств

Мы можем исправить вышеуказанную ошибку TypeScript тремя разными способами.

  • Объявление типов с использованием интерфейса

Ошибка возникает из-за того, что мы добавили свойство title как обязательное свойство для компонента App – так что нам нужно указать это внутри компонента App.

Откройте файл src/App.tsx и замените его следующим кодом:

import { FC } from 'react';interface AppProps {  title: string;}const App: FC<AppProps> = () => {  return <div>App</div>;};export default App;

Как вы можете видеть выше, мы добавили дополнительный интерфейс AppProps для указания, какие свойства принимает компонент. Мы также использовали интерфейс AppProps после FC в угловых скобках.

Это хорошая практика и рекомендуется начинать имя интерфейса с заглавной буквы, например, AppProps в нашем случае.

Теперь, после этого изменения, ошибка TypeScript исчезнет, как вы можете видеть ниже:

8_no_prop_error
Добавление Prop Types к компоненту

Вот как мы указываем, какие props принимает определенный компонент.

  • Объявление типов с использованием ключевого слова type

Мы также можем объявлять типы props с использованием ключевого слова type.

Так что откройте файл App.tsx и измените следующий код:

import { FC } from 'react';interface AppProps {  title: string;}const App: FC<AppProps> = () => {  return <div>App</div>;};export default App;

на следующий код:

import { FC } from 'react';type AppProps = {  title: string;};const App: FC<AppProps> = () => {  return <div>App</div>;};export default App;

Здесь, вместо объявления interface, мы использовали объявление type. Теперь код будет работать без ошибок TypeScript.

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

  • Использование встроенного объявления типов

Третий способ объявления типа – это определение встроенных типов, как показано ниже:

const App = ({ title }: { title: string }) => {  return <div>App</div>;};export default App;

Как вы видите, мы удалили использование FC, так как это не нужно, и при деструктуризации свойства title мы определили его тип.

Итак, из этих трех способов вы можете использовать любой, который вам нравится. Я всегда предпочитаю использовать интерфейс с FC. Таким образом, если я захочу позже добавить больше props, код не будет выглядеть сложным (что произойдет, если вы определите встроенные типы).

Теперь давайте использовать prop title и отобразить его на UI.

Замените содержимое файла App.tsx на следующий код:

import { FC } from 'react';interface AppProps {  title: string;}const App: FC<AppProps> = ({ title }) => {  return <h1>{title}</h1>;};export default App;

Как вы видите, мы используем интерфейс с FC, а также деструктурируем свойство title и отображаем его на экране.

Теперь откройте файл src/index.css и добавьте в него следующий CSS:

h1 {  text-align: center;}

Если вы проверите приложение в браузере, вы увидите, что заголовок с текстом TypeScript Demo правильно отображается.

10_title_displayed
Заголовок приложения отображается правильно

Как создать приложение со списком случайных пользователей

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

Для этого мы будем использовать API Random User Generator.

Вот URL API, который мы будем использовать:

https://randomuser.me/api/?results=10

Давайте сначала установим библиотеку Axios с помощью npm, чтобы мы могли сделать вызов API с ее помощью.

Выполните следующую команду, чтобы установить библиотеку Axios:

npm install axios

После установки перезапустите приложение, выполнив команду npm run dev.

Теперь замените содержимое файла App.tsx на следующий код:

import axios from 'axios';import { FC, useEffect } from 'react';interface AppProps {  title: string;}const App: FC<AppProps> = ({ title }) => {  useEffect(() => {    const getUsers = async () => {      try {        const { data } = await axios.get(          'https://randomuser.me/api/?results=10'        );        console.log(data);      } catch (error) {        console.log(error);      }    };    getUsers();  }, []);  return <h1>{title}</h1>;};export default App;

Как видите выше, мы добавили хук useEffect, где выполняем API-запрос для получения списка пользователей.

Теперь, если вы откроете консоль в браузере, вы увидите отображение ответа API в консоли.

11_api_response
Ответ API

Как видите, мы успешно получаем список из 10 случайных пользователей, и фактический список пользователей находится в свойстве results ответа.

Как сохранить список пользователей в состоянии

Теперь давайте сохранем этих пользователей в состоянии, чтобы мы могли отобразить их на экране.

Внутри компонента App объявите новое состояние со значением пустого массива, вот так:

const [users, setUsers ] = useState([]);

И вызовите функцию setUsers для сохранения пользователей в хуке useEffect после вызова API.

Таким образом, ваш компонент App будет выглядеть так:

import axios from 'axios';import { FC, useEffect, useState } from 'react';interface AppProps {  title: string;}const App: FC<AppProps> = ({ title }) => {  const [users, setUsers] = useState([]);  useEffect(() => {    const getUsers = async () => {      try {        const { data } = await axios.get(          'https://randomuser.me/api/?results=10'        );        console.log(data);        setUsers(data.results);      } catch (error) {        console.log(error);      }    };    getUsers();  }, []);  return <h1>{title}</h1>;};export default App;

Как видите здесь, мы вызываем функцию setUsers со значением data.results.

Как отобразить пользователей на пользовательском интерфейсе

Теперь давайте отобразим имя и электронную почту каждого отдельного пользователя на экране.

Если вы проверите вывод в консоль, вы увидите, что у каждого объекта есть свойство name, содержащее имя и фамилию пользователя. Мы можем объединить их, чтобы отобразить полное имя.

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

12_required_properties
Изучение ответа API

Замените содержимое файла App.tsx следующим кодом:

import axios from 'axios';import { FC, useEffect, useState } from 'react';interface AppProps {  title: string;}const App: FC<AppProps> = ({ title }) => {  const [users, setUsers] = useState([]);  useEffect(() => {    const getUsers = async () => {      try {        const { data } = await axios.get(          'https://randomuser.me/api/?results=10'        );        console.log(data);        setUsers(data.results);      } catch (error) {        console.log(error);      }    };    getUsers();  }, []);  return (    <div>      <h1>{title}</h1>      <ul>        {users.map(({ login, name, email }) => {          return (            <li key={login.uuid}>              <div>                Имя: {name.first} {name.last}              </div>              <div>Электронная почта: {email}</div>              <hr />            </li>          );        })}      </ul>    </div>  );};export default App;

Как видите, мы используем метод map массива для перебора массива users, и мы используем деструктуризацию объекта для деструктуризации свойств login, name и email индивидуальных объектов user. Также мы отображаем имя и электронную почту пользователя в виде неупорядоченного списка.

Но вы увидите ошибки TypeScript в файле, как показано ниже:

13_user_errors
Ошибки типов свойств пользователей

Это происходит потому, что, как вы видите выше, по умолчанию TypeScript предполагает, что тип массива users это never[] – поэтому он не может определить, какие свойства содержит массив users.

Это означает, что нам нужно указать все используемые свойства вместе с их типами.

Итак, теперь объявите новый интерфейс после интерфейса AppProps таким образом:

interface Users {  name: {    first: string;    last: string;  };  login: {    uuid: string;  };  email: string;}

Здесь мы указываем, что каждый отдельный user будет объектом с свойствами name, login и email. Мы также указываем тип данных каждого свойства.

Как видно, у каждого объекта user, поступающего из API, есть много других свойств, таких как phone, location и другие. Но нам нужно указать только те свойства, которые мы используем в коде.

Теперь измените объявление массива users в useState следующим образом:

const [users, setUsers] = useState([]);

на это:

const [users, setUsers] = useState<Users[]>([]);

Здесь мы указываем, что users это массив объектов типа Users, который мы объявили.

Теперь, если вы проверите файл App.tsx, вы увидите, что там нет ошибок TypeScript.

14_no_type_error
Ошибка типов свойств исправлена

И вы сможете увидеть список из 10 случайных пользователей, отображаемый на экране:

15_random_users
Список случайных пользователей, отображаемый на экране

Как вы видели ранее, мы объявили интерфейс Users следующим образом:

interface Users {  name: {    first: string;    last: string;  };  login: {    uuid: string;  };  email: string;}

Но когда у вас есть вложенные свойства, они записываются так:

interface Name {  first: string;  last: string;}interface Login {  uuid: string;}interface Users {  name: Name;  login: Login;  email: string;}

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

Итак, экспортируем все вышеуказанные интерфейсы как импорт по имени. Так код будет выглядеть так:

export interface Name {  first: string;  last: string;}export interface Login {  uuid: string;}export interface Users {  name: Name;  login: Login;  email: string;}

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

type Name = {  first: string;  last: string;};type Login = {  uuid: string;};type Users = {  name: Name;  login: Login;  email: string;};

Как создать отдельный компонент пользователя

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

Создайте папку components внутри папки src и создайте файл User.tsx внутри нее. Затем добавьте следующее содержимое в этот файл:

const User = ({ login, name, email }) => {  return (    <li key={login.uuid}>      <div>        Имя: {name.first} {name.last}      </div>      <div>Email: {email}</div>      <hr />    </li>  );};export default User;

Если вы сохраните файл, вы увидите ошибки TypeScript снова.

16_user_component_props_error
Ошибка объявления свойств компонента пользователя

Так что снова нам нужно указать, какие свойства будет получать компонент User. Мы также должны указать тип данных для каждого из них.

Так что обновленный файл User.tsx будет выглядеть так:

import { FC } from 'react';import { Login, Name } from '../App';interface UserProps {  login: Login;  name: Name;  email: string;}const User: FC<UserProps> = ({ login, name, email }) => {  return (    <li key={login.uuid}>      <div>        Имя: {name.first} {name.last}      </div>      <div>Email: {email}</div>      <hr />    </li>  );};export default User;

Как вы видите выше, мы объявили интерфейс UserProps выше, и указали его для компонента User с помощью FC.

Также обратите внимание, что мы не указываем тип данных для свойств name и login. Вместо этого мы используем экспортируемые типы из файла App.tsx:

import { Login, Name } from '../App';

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

Теперь мы можем использовать этот компонент User внутри файла App.tsx.

Измените следующий код:

{users.map(({ login, name, email }) => {  return (    <li key={login.uuid}>      <div>        Имя: {name.first} {name.last}      </div>      <div>Email: {email}</div>      <hr />    </li>  );})}

на этот код:

{users.map(({ login, name, email }) => {  return <User key={login.uuid} name={name} email={email} />;})}

Как вы, возможно, знаете, при использовании метода map для массива, мы должны указать key для родительского элемента, который в нашем случае является User. Поэтому мы добавили свойство key при использовании компонента User, как показано выше.

Это означает, что нам не нужен ключ внутри компонента User, поэтому мы можем удалить ключ и проп login из компонента User.

Таким образом, обновленный компонент User будет выглядеть так:

import { FC } from 'react';import { Name } from '../App';interface UserProps {  name: Name;  email: string;}const User: FC<UserProps> = ({ name, email }) => {  return (    <li>      <div>        Имя: {name.first} {name.last}      </div>      <div>Email: {email}</div>      <hr />    </li>  );};export default User;

Как видите, мы удалили проп login из интерфейса, а также деструктурировали его. Приложение все еще работает без проблем, как вы можете видеть ниже.

17_working_with_refactor
Список случайных пользователей, отображаемый на пользовательском интерфейсе

Как создать отдельный файл для объявления типов

Как вы можете видеть, файл App.tsx стал довольно большим из-за объявлений интерфейса. Обычно используется отдельный файл только для объявления типов.

Поэтому создайте файл App.types.ts внутри папки src и переместите все объявления типов из компонента App в файл App.types.ts:

export interface AppProps {  title: string;}export interface Name {  first: string;  last: string;}export interface Login {  uuid: string;}export interface Users {  name: Name;  login: Login;  email: string;}

Обратите внимание, что в приведенном выше коде мы также экспортируем компонент AppProps.

Теперь обновите файл App.tsx, чтобы использовать эти типы, как показано ниже:

import axios from 'axios';import { FC, useEffect, useState } from 'react';import { AppProps, Users } from './App.types';import User from './components/User';const App: FC<AppProps> = ({ title }) => {  const [users, setUsers] = useState<Users[]>([]);    // ...};export default App;

Как вы видите выше, мы импортируем AppProps и Users из файла App.types:

import { AppProps, Users } from './App.types';

И ваш файл User.tsx будет выглядеть следующим образом:

import { FC } from 'react';import { Name } from '../App.types';interface UserProps {  name: Name;  email: string;}const User: FC<UserProps> = ({ name, email }) => {  return (    <li>      <div>        Имя: {name.first} {name.last}      </div>      <div>Email: {email}</div>      <hr />    </li>  );};export default User;

Как вы видите выше, мы импортируем Name из файла App.types.

import { Name } from '../App.types';

Как отобразить индикатор загрузки

Всегда полезно отображать индикатор загрузки во время выполнения API-запроса для отображения чего-либо.

Поэтому давайте добавим новое состояние isLoading внутри компонента App:

const [isLoading, setIsLoading] = useState(false);

Как видите, мы не указали тип данных при объявлении состояния:

const [isLoading, setIsLoading] = useState<boolean>(false);

Это происходит потому, что при присвоении любого начального значения (false в нашем случае) TypeScript автоматически выводит тип данных, которые мы будем хранить – в нашем случае это boolean.

Когда мы объявляем состояние users, не было ясно, что мы будем хранить только по начальному значению пустого массива []. Поэтому нам нужно было указать его тип так:

const [users, setUsers] = useState<Users[]>([]);

Теперь измените код useEffect на следующий код:

useEffect(() => {  const getUsers = async () => {    try {      setIsLoading(true);      const { data } = await axios.get(        'https://randomuser.me/api/?results=10'      );      console.log(data);      setUsers(data.results);    } catch (error) {      console.log(error);    } finally {      setIsLoading(false);    }  };  getUsers();}, []);

Здесь мы вызываем setIsLoading со значением true перед вызовом API. Внутри блока finally мы возвращаем его обратно в значение false.

Код, написанный внутри блока finally, будет выполняться всегда, независимо от успеха или неудачи. Поэтому, независимо от того, успешен ли вызов API или нет, мы должны скрыть сообщение о загрузке, и для этого мы используем блок finally.

Теперь мы можем использовать значение состояния isLoading для отображения сообщения о загрузке на экране.

После тега h1 и перед тегом ul добавьте следующий код:

{isLoading && <p>Loading...</p>}

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

18_loading
Отображение индикации загрузки

Таким образом, это лучший пользовательский опыт.

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

Так что сначала мы видим набор из 10 случайных пользователей, а затем сразу после этого мы видим другой набор случайных пользователей без перезагрузки страницы.

Это происходит потому, что мы используем React версии 18 (которую вы можете проверить в файле package.json) и React.StrictMode внутри файла src/main.tsx.

И с версией 18 React, при использовании React.StrictMode каждый хук useEffect выполняется дважды, даже если не указано зависимостей.

Это происходит только в среде разработки и не в продакшне, когда вы развертываете приложение.

Из-за этого API-запрос выполняется дважды. Так как API случайных пользователей возвращает новый набор случайных пользователей при каждом вызове API, мы устанавливаем другой набор пользователей в массив users с помощью вызова setUsers внутри хука useEffect.

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

Если вы не хотите такого поведения во время разработки, вы можете удалить React.StrictMode из файла main.tsx.

Измените следующий код:

import React from 'react';import ReactDOM from 'react-dom/client';import App from './App.tsx';import './index.css';ReactDOM.createRoot(document.getElementById('root')!).render(  <React.StrictMode>    <App title='TypeScript Demo' />  </React.StrictMode>);

на этот код:

import ReactDOM from 'react-dom/client';import App from './App.tsx';import './index.css';ReactDOM.createRoot(document.getElementById('root')!).render(  <App title='TypeScript Demo' />);

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

Как загрузить пользователей по нажатию кнопки

Теперь, вместо вызова API при загрузке страницы, давайте добавим кнопку “показать пользователей” и будем делать вызов API при клике на эту кнопку.

Так что после тега h1 добавьте новую кнопку, как показано ниже:

<button onClick={handleClick}>Показать пользователей</button>

Теперь добавьте метод handleClick внутри компонента App и переместите весь код из функции getUsers в метод handleClick:

const handleClick = async () => {  try {    setIsLoading(true);    const { data } = await axios.get('https://randomuser.me/api/?results=10');    console.log(data);    setUsers(data.results);  } catch (error) {    console.log(error);  } finally {    setIsLoading(false);  }};

Теперь вы можете удалить или закомментировать хук useEffect, так как он больше не нужен.

Ваш обновленный файл App.tsx будет выглядеть так:

import axios from 'axios';import { FC, useState } from 'react';import { AppProps, Users } from './App.types';import User from './components/User';const App: FC = ({ title }) => {  const [users, setUsers] = useState([]);  const [isLoading, setIsLoading] = useState(false);  const handleClick = async () => {    try {      setIsLoading(true);      const { data } = await axios.get('https://randomuser.me/api/?results=10');      console.log(data);      setUsers(data.results);    } catch (error) {      console.log(error);    } finally {      setIsLoading(false);    }  };  return (    

{title}

{isLoading &&

Загрузка...

}
    {users.map(({ login, name, email }) => { return ; })}
);};export default App;

Теперь, если вы проверите приложение, вы увидите, что пользователи загружаются только при нажатии кнопки “показать пользователей”. Мы также видим сообщение о загрузке.

19_load_on_click
Пользователи загружены по нажатию кнопки

Как обрабатывать изменение событий

Теперь давайте добавим поле ввода. Когда мы что-то вводим в это поле, мы будем отображать введенный текст под этим полем ввода.

Добавьте поле ввода после кнопки, как показано ниже:

<input type='text' onChange={handleChange} />

И объявите новое состояние для хранения введенного значения, как показано ниже:

const [username, setUsername] = useState('');

Теперь добавьте метод handleChange внутри компонента App, как показано ниже:

 const handleChange = (event) => {  setUsername(event.target.value);};

Однако вы увидите, что мы получаем ошибку TypeScript для параметра event.

20_event_type
Отсутствует тип события, ошибка TypeScript

С TypeScript нам всегда необходимо указывать тип каждого параметра функции.

Здесь TypeScript не может определить тип параметра event.

Чтобы узнать тип параметра event, мы можем изменить следующий код:

<input type='text' onChange={handleChange} />

на следующий код:

<input type='text' onChange={(event) => {}} />

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

Если навести курсор мыши на параметр event, вы сможете увидеть точный тип события, который мы можем использовать в нашей функции handleChange, как показано ниже:

21_change_event_type
Определение типа события TypeScript с использованием встроенной функции

Теперь вы можете изменить следующий код:

<input type='text' onChange={(event) => {}} />

на этот код:

<input type='text' onChange={handleChange} />

Теперь давайте отобразим значение переменной состояния username ниже поля ввода:

<input type='text' onChange={handleChange} /><div>{username}</div>

Если вы проверите приложение сейчас, вы увидите введенный текст, отображаемый ниже поля ввода.

22_username
Отображение введенного пользователем текста на пользовательском интерфейсе

Спасибо за чтение

Вот и все для этого учебного пособия. Надеюсь, вы многое из него усвоили.

Хотите посмотреть видеоверсию этого учебника? Вы можете посмотреть это видео.

Вы можете найти полный исходный код для этого приложения в этом репозитории.

Если вы хотите овладеть JavaScript, ES6+, React и Node.js с помощью понятного контента, загляните на мой YouTube-канал. Не забудьте подписаться.

Хотите быть в курсе новых материалов по JavaScript, React и Node.js? Подпишитесь на мою страницу LinkedIn.

Изучение создания приложения-управления расходами с использованием React и TypeScript


Leave a Reply

Your email address will not be published. Required fields are marked *