Привет, Хабр!
Частенько сталкиваются с проблемой поддержания типовой безопасности в 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 рассказывают в рамках практических онлайн-курсов. С полным каталогом курсов можно ознакомиться по ссылке.
ссылка на оригинал статьи https://habr.com/ru/articles/829626/
Добавить комментарий