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

d76255680a858e558b47843a077f864d.jpg

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

Частенько сталкиваются с проблемой поддержания типовой безопасности в 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 = ({ label, onClick }) => (
  
);

export default Button;

Пример типов для State:

import React, { useState } from 'react';

type CounterState = {
  count: number;
};

const Counter: React.FC = () => {
  const [state, setState] = useState({ count: 0 });

  const increment = () => {
    setState({ count: state.count + 1 });
  };

  return (
    

Count: {state.count}

); }; 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 (
    

Count: {count}

); }; export default CounterComponent;

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

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

import { useState } from 'react';

type ChangeEvent = React.ChangeEvent;

/**
 * Пользовательский хук useForm.
 * @param initialValues Начальные значения формы.
 * @returns Текущие значения формы, функция для обработки изменений и функция для сброса формы.
 */
function useForm(initialValues: T) {
  const [values, setValues] = useState(initialValues);

  const handleChange = (event: ChangeEvent) => {
    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({
    username: '',
    email: ''
  });

  const handleSubmit = (event: React.FormEvent) => {
    event.preventDefault();
    console.log(values);
    resetForm();
  };

  return (
    
); }; export default FormComponent;

Пользовательские хуки могут быть использованы для реализации сложных логик. И вот пример создания пользовательского хука для получения данных с API:

import { useState, useEffect } from 'react';

interface ApiResponse {
  data: T | null;
  loading: boolean;
  error: string | null;
}

/**
 * Пользовательский хук useFetch.
 * @param url URL для запроса.
 * @returns Состояние запроса, данные, ошибка и статус загрузки.
 */
function useFetch(url: string): ApiResponse {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(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('https://jsonplaceholder.typicode.com/users');

  if (loading) return 

Loading...

; if (error) return

Error: {error}

; return (
    {data?.map(user => (
  • {user.name}
  • ))}
); }; export default UserList;

Переходим к следующей важной теме — универсальные компоненты с дженериками.

Универсальные компоненты с Generic Components

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

Пример создания простого компонента списка, который может принимать любой тип данных:

import React from 'react';

interface ListProps {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List({ items, renderItem }: ListProps): React.ReactElement {
  return (
    
    {items.map((item, index) => (
  • {renderItem(item)}
  • ))}
); } 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 (
    

User List

{user.name}} />
); }; export default App;

Универсальные таблицы — это еще один пример компонентов, которые могут выиграть от использования Generics. Пример:

import React from 'react';

interface TableProps {
  columns: (keyof T)[];
  data: T[];
  renderCell: (item: T, column: keyof T) => React.ReactNode;
}

function Table({ columns, data, renderCell }: TableProps): React.ReactElement {
  return (
    
          {columns.map((column) => (
            
          ))}
        
        {data.map((item, rowIndex) => (
          
            {columns.map((column) => (
              
            ))}
          
        ))}
      
{String(column)}
{renderCell(item, column)}
); } 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 (
    

Product Table

item[column]} /> ); }; export default App;

Универсальные формы, которые могут принимать различные типы данных для различных полей, также могут быть реализованы с помощью Generics:

import React, { useState } from 'react';

interface FormProps {
  initialValues: T;
  renderForm: (values: T, handleChange: (e: React.ChangeEvent) => void) => React.ReactNode;
  onSubmit: (values: T) => void;
}

function Form({ initialValues, renderForm, onSubmit }: FormProps): React.ReactElement {
  const [values, setValues] = useState(initialValues);

  const handleChange = (e: React.ChangeEvent) => {
    const { name, value } = e.target;
    setValues({
      ...values,
      [name]: value
    });
  };

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    onSubmit(values);
  };

  return (
    
{renderForm(values, handleChange)} ); } 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 (
    

User Profile Form

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

© Habrahabr.ru