Привет, Хабр!
Частенько сталкиваются с проблемой поддержания типовой безопасности в React-проекте. Код разрастается, и управление типами становится всё сложнее. Ошибки, вызванные неправильной типизацией, приводят к крашам и длительным отладкам. Тогда приходит время внедрения TypeScript!
В статье рассмотрим как TypeScript может помочь решить проблемы с типизацией и сделать React-код идеально типизированным.
Строгая типизация и Type Inference в TypeScript
Строгий режим TypeScript strict — это конфигурация, которая включает ряд некоторых строгих проверок типов.
Чтобы включить строгий режим в проекте, необходимо изменить файл конфигурации TypeScript tsconfig.json:
{
  "compilerOptions": {
    "strict": true
  }
}Это автоматом включает несколько поднастроек:
- noImplicitAny: отключает неявное присвоение типа- any. Все переменные должны иметь явный тип.
- strictNullChecks: обспечивает строгую проверку- nullи- undefined. Это предотвращает использование переменных, которые могут быть- nullили- undefined, без соответствующей проверки.
- strictFunctionTypes: включает строгие проверки типов для функций.
- strictPropertyInitialization: проверяет, что все обязательные свойства инициализируются в конструкторе класса.
- noImplicitThis: отлючает неявное присвоение типа- anyдля- thisв функциях.
- alwaysStrict: включает строгий режим JavaScript во всех файлах.
Пример строгого режима:
function add(a: number, b: number): number {
  return a + b;
}
let result = add(2, 3); // OK
let result2 = add('2', 3); // ошибка компиляции: тип 'string' не может быть присвоен параметру типа 'number'Вывод типов (Type Inference) позволяет автоматически определяет типы переменных и выражений на основе их значения или контекста использования.
Когда мы объявляем переменную или функцию без явного указания типа, TypeScript пытается вывести тип автоматом на основе присвоенного значен:
let x = 3; // TypeScript выводит тип 'number'
let y = 'privet'; // TypeScript выводит тип 'string'
let z = { name: 'Artem', age: 30 }; // TypeScript выводит тип { name: string; age: number }TypeScript автоматически определяет тип переменных x, y и z на основе их значений. 
Иногда вывод типов может быть недостаточно точным или полезным, например тут:
let items = ['apple', 'banana', 42]; // Тип выводится как (string | number)[]Мссив items имеет тип (string | number)[], что может не соответствовать ожидаемому поведению. В таких случаях лучше явно указать тип.
Переходим к следующему пункту - правильной типизации Props и State в React с TypeScript
Правильная типизация Props и State в React с TypeScript
Правильное определение типов для Props и State помогает создать более структурированный код.
В TypeScript есть два основных способа определения типов: интерфейсы и типы. Хотя оба подхода имеют схожие возможности, есть некоторые различия:
Интерфейсы:
- Обычно их используют для определения структур данных и контрактов для публичных API. 
- Поддерживают декларативное слияние. 
- Лучше подходят для объектов с множеством свойств. 
Типы:
- Используются для определения алиасов типов, особенно для объединений и пересечений типов. 
- Более гибкие. 
- Лучше подходят для простых объектов, состояний и внутренних компонентов. 
Пример интерфейсов для Props:
import React from 'react';
interface ButtonProps {
  label: string;
  onClick: () => void;
}
const Button: React.FC<ButtonProps> = ({ label, onClick }) => (
  <button onClick={onClick}>{label}</button>
);
export default Button;Пример типов для State:
import React, { useState } from 'react';
type CounterState = {
  count: number;
};
const Counter: React.FC = () => {
  const [state, setState] = useState<CounterState>({ count: 0 });
  const increment = () => {
    setState({ count: state.count + 1 });
  };
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};
export default Counter;Для указания обязательных свойств можно использовать просто имя свойства, а для необязательных добавляйте знак ?:
interface UserProps {
  name: string; // обязательное свойство
  age?: number; // необязательное свойство
}Для типизации сложных объектов и массивов можно юзать вложенные интерфейсы или типы:
interface Address {
  street: string;
  city: string;
}
interface UserProps {
  name: string;
  age?: number;
  address: Address; // вложенный объект
  hobbies: string[]; // массив строк
}
Union типы позволяют объединять несколько типов, а intersection типы — пересекать их:
type Status = 'success' | 'error' | 'loading';
interface Response {
  data: string;
}
type ApiResponse = Response & { status: Status };Переходим к следующему поинту - пользовательские хуки.
Пользовательские хуки
Пользовательские хуки в React позволяют инкапсулировать и переиспользовать логику состояния и побочных эффектов.
Пользовательский хук — это функция, имя которой начинается с use, и которая может использовать другие хуки внутри себя. С помощью этого можно выносить повторяющуюся логику состояния или побочных эффектов в отдельные функции, которые можно переиспользовать в различных компонентах.
Пример создания простого пользовательского хука для управления состоянием счетчика:
import { useState } from 'react';
/**
 * Пользовательский хук useCounter.
 * @param initialValue начальное значение счетчика.
 * @returns Текущее значение счетчика и функции для его увеличения и сброса.
 */
function useCounter(initialValue: number) {
  const [count, setCount] = useState(initialValue);
  const increment = () => setCount(count + 1);
  const reset = () => setCount(initialValue);
  return { count, increment, reset };
}
export default useCounter;Этот хук можно использовать в любом компоненте:
import React from 'react';
import useCounter from './useCounter';
const CounterComponent: React.FC = () => {
  const { count, increment, reset } = useCounter(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
};
export default CounterComponent;Generics в TypeScript позволяют создавать хуки, которые могут работать с различными типами данных.
Пример создания пользовательского хука для управления состоянием формы:
import { useState } from 'react';
type ChangeEvent<T> = React.ChangeEvent<T>;
/**
 * Пользовательский хук useForm.
 * @param initialValues Начальные значения формы.
 * @returns Текущие значения формы, функция для обработки изменений и функция для сброса формы.
 */
function useForm<T>(initialValues: T) {
  const [values, setValues] = useState<T>(initialValues);
  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { name, value } = event.target;
    setValues({
      ...values,
      [name]: value
    });
  };
  const resetForm = () => setValues(initialValues);
  return { values, handleChange, resetForm };
}
export default useForm;Этот хук также можно использовать для управления состоянием формы в любом компоненте:
import React from 'react';
import useForm from './useForm';
interface FormValues {
  username: string;
  email: string;
}
const FormComponent: React.FC = () => {
  const { values, handleChange, resetForm } = useForm<FormValues>({
    username: '',
    email: ''
  });
  const handleSubmit = (event: React.FormEvent) => {
    event.preventDefault();
    console.log(values);
    resetForm();
  };
  return (
    <form onSubmit={handleSubmit}>
      <label>
        Username:
        <input
          type="text"
          name="username"
          value={values.username}
          onChange={handleChange}
        />
      </label>
      <label>
        Email:
        <input
          type="email"
          name="email"
          value={values.email}
          onChange={handleChange}
        />
      </label>
      <button type="submit">Submit</button>
    </form>
  );
};
export default FormComponent;Пользовательские хуки могут быть использованы для реализации сложных логик. И вот пример создания пользовательского хука для получения данных с API:
import { useState, useEffect } from 'react';
interface ApiResponse<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}
/**
 * Пользовательский хук useFetch.
 * @param url URL для запроса.
 * @returns Состояние запроса, данные, ошибка и статус загрузки.
 */
function useFetch<T>(url: string): ApiResponse<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const result = await response.json();
        setData(result);
      } catch (error) {
        setError(error.message);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [url]);
  return { data, loading, error };
}
export default useFetch;Этот хук можно использовать для получения данных в компоненте:
import React from 'react';
import useFetch from './useFetch';
interface User {
  id: number;
  name: string;
}
const UserList: React.FC = () => {
  const { data, loading, error } = useFetch<User[]>('https://jsonplaceholder.typicode.com/users');
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return (
    <ul>
      {data?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};
export default UserList;Переходим к следующей важной теме - универсальные компоненты с дженериками.
Универсальные компоненты с Generic Components
С универсальными компонентами можно создавать списки, таблицы или формы, где структура данных может варьироваться.
Пример создания простого компонента списка, который может принимать любой тип данных:
import React from 'react';
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>): React.ReactElement {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}
export default List;Компонент List может быть использован с любыми типами данных:
import React from 'react';
import List from './List';
interface User {
  id: number;
  name: string;
}
const users: User[] = [
  { id: 1, name: 'Kolya' },
  { id: 2, name: 'Vanya' },
];
const App: React.FC = () => {
  return (
    <div>
      <h1>User List</h1>
      <List items={users} renderItem={(user) => <span>{user.name}</span>} />
    </div>
  );
};
export default App;Универсальные таблицы — это еще один пример компонентов, которые могут выиграть от использования Generics. Пример:
import React from 'react';
interface TableProps<T> {
  columns: (keyof T)[];
  data: T[];
  renderCell: (item: T, column: keyof T) => React.ReactNode;
}
function Table<T>({ columns, data, renderCell }: TableProps<T>): React.ReactElement {
  return (
    <table>
      <thead>
        <tr>
          {columns.map((column) => (
            <th key={String(column)}>{String(column)}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map((item, rowIndex) => (
          <tr key={rowIndex}>
            {columns.map((column) => (
              <td key={String(column)}>{renderCell(item, column)}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}
export default Table;Этот компонент можно использовать для отображения данных любого типа:
import React from 'react';
import Table from './Table';
interface Product {
  id: number;
  name: string;
  price: number;
}
const products: Product[] = [
  { id: 1, name: 'Laptop', price: 1000 },
  { id: 2, name: 'Phone', price: 500 },
];
const App: React.FC = () => {
  return (
    <div>
      <h1>Product Table</h1>
      <Table
        columns={['id', 'name', 'price']}
        data={products}
        renderCell={(item, column) => item[column]}
      />
    </div>
  );
};
export default App;Универсальные формы, которые могут принимать различные типы данных для различных полей, также могут быть реализованы с помощью Generics:
import React, { useState } from 'react';
interface FormProps<T> {
  initialValues: T;
  renderForm: (values: T, handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void) => React.ReactNode;
  onSubmit: (values: T) => void;
}
function Form<T>({ initialValues, renderForm, onSubmit }: FormProps<T>): React.ReactElement {
  const [values, setValues] = useState<T>(initialValues);
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setValues({
      ...values,
      [name]: value
    });
  };
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    onSubmit(values);
  };
  return (
    <form onSubmit={handleSubmit}>
      {renderForm(values, handleChange)}
      <button type="submit">Submit</button>
    </form>
  );
}
export default Form;Использование этого компонента для создания формы:
import React from 'react';
import Form from './Form';
interface UserProfile {
  username: string;
  email: string;
}
const App: React.FC = () => {
  const initialValues: UserProfile = { username: '', email: '' };
  const handleSubmit = (values: UserProfile) => {
    console.log(values);
  };
  return (
    <div>
      <h1>User Profile Form</h1>
      <Form
        initialValues={initialValues}
        renderForm={(values, handleChange) => (
          <>
            <label>
              Username:
              <input type="text" name="username" value={values.username} onChange={handleChange} />
            </label>
            <label>
              Email:
              <input type="email" name="email" value={values.email} onChange={handleChange} />
            </label>
          </>
        )}
        onSubmit={handleSubmit}
      />
    </div>
  );
};
export default App;На этом моменте хотелось уже закончить статью, но есть еще один важный поинт - внешние библиотеки.
Интеграция и типизация внешних библиотек
Большинство популярных JS-библиотек имеют типы, которые можно установить через npm или yarn. Эти типы находятся в специальном пространстве имен @types.
Установка типов через npm:
npm install @types/library-name
Установка типов через yarn:
yarn add @types/library-name
Пример установки типов для библиотеки lodash:
npm install lodash @types/lodash
После установки типов можно использовать библиотеку с полной типовой поддержкой. Пример с использованием lodash:
import _ from 'lodash';
const numbers: number[] = [1, 2, 3, 4, 5];
const doubled = _.map(numbers, num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]TypeScript автоматически распознает типы, предоставляемые библиотекой lodash, благодаря установленным типам.
Но как мы знаем не все в этом мире идеально и поэтому – не все библиотеки имеют готовые типы. В таких случаях можно создать собственные декларации типов, чтобы избежать использования типа any.
Предположим, есть библиотека example-library, у которой нет готовых типов. Создадим собственные декларации типов для этой библиотеки.
- Создаем файл с типами, например - example-library.d.ts.
- Определяем типы для используемых функций и объектов библиотеки. 
Пример:
// example-library.d.ts
declare module 'example-library' {
  export function exampleFunction(param: string): number;
  export const exampleConstant: string;
}После создания этого файла можно использовать библиотеку с типовой поддержкой:
import { exampleFunction, exampleConstant } from 'example-library';
const result: number = exampleFunction('test');
console.log(result); 
console.log(exampleConstant);Флаг skipLibCheck в файле tsconfig.json позволяет пропускать проверку типов библиотек. Полезно, когда типы библиотек содержат ошибки, но очень хочется продолжить компиляцию проекта.
{
  "compilerOptions": {
    "skipLibCheck": true
  }
}Финальные слова
TypeScript в React-проектах — это не просто рекомендация, а необходимость для тех, кто хочет создать надежное, масштабируемое, а самое главное - легкое в сопровождении приложение.
Больше про языки программирования эксперты OTUS рассказывают в рамках практических онлайн-курсов. С полным каталогом курсов можно ознакомиться по ссылке.
Комментарии (4)
 - teilarerJs26.07.2024 07:43+3- Вы написали хотя бы одну строчку на typescript в своей жизни, ну если честно? - Пример строгого режима: - function add(a: number, b: number): number { return a + b;}let result = add(2, 3); // OKlet result2 = add('2', 3); // ошибка компиляции: тип 'string' не может быть присвоен параметру типа 'number'- Это не пример строгого режима, даже если выключить вообще все флаги строгости, ts подсветит ошибку. - Иногда вывод типов может быть недостаточно точным или полезным, например тут: - Массив - itemsимеет тип- (string | number)[], что может не соответствовать ожидаемому поведению. В таких случаях лучше явно указать тип.- let items = ['apple', 'banana', 42]; // Тип выводится как (string | number)[]- В каком месте здесь вывод типов недостаточно точный/полезный, и не соответствует ожидаемому поведению? - Интерфейсы: - Лучше подходят для объектов с множеством свойств. 
 - Чушь. О различиях доходчиво написано в документации. 
 https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces
 По реакту нейросеть сгенерировала нормальный код, поздравляю.
 - dopusteam26.07.2024 07:43- Мссив items имеет тип (string | number)[], что может не соответствовать ожидаемому поведению. В таких случаях лучше явно указать тип. - А какой тип вы ожидаете? 
 
           
 
Keeper10
Зачем его делать идеально типизированным?