TypeScript + React: путь к идеально типизированному коду

от автора

Привет, Хабр!

Частенько сталкиваются с проблемой поддержания типовой безопасности в 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, у которой нет готовых типов. Создадим собственные декларации типов для этой библиотеки.

  1. Создаем файл с типами, например example-library.d.ts.

  2. Определяем типы для используемых функций и объектов библиотеки.

Пример:

// 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 рассказывают в рамках практических онлайн-курсов. С полным каталогом курсов можно ознакомиться по ссылке.


ссылка на оригинал статьи https://habr.com/ru/articles/829626/