Основы типизации HOC с TypeScript
Начнем с базового шаблона HOC. Как вы уже знаете из предыдущих лекций, HOC — это функция, которая принимает компонент и возвращает новый компонент с добавленной функциональностью.
Для начала, освежим базовый HOC без типизации:
import React from 'react';
// Небольшой HOC, который добавляет логику загрузки
const withLoading = (Component: React.ComponentType) => {
return (props: any) => {
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
setTimeout(() => setIsLoading(false), 3000);
}, []);
return isLoading ? <p>Loading...</p> : <Component {...props} />;
};
};
Этот HOC добавляет индикатор загрузки при рендеринге компонента. Но здесь полно «магии» (читай: props: any) и полное отсутствие типизации! Исправим это.
Типизация базового HOC
Чтобы типизировать HOC, для начала следует понять, какие пропсы он принимает и передает.
Компонент, который оборачивается HOC, называется WrappedComponent. Его пропсы можно типизировать так:
type WrappedComponentProps = {
title: string;
};
Теперь передадим этот тип в HOC:
import React from 'react';
const withLoading = <P extends object>(Component: React.ComponentType<P>) => {
return (props: P) => {
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
setTimeout(() => setIsLoading(false), 3000);
}, []);
return isLoading ? <p>Loading...</p> : <Component {...props} />;
};
};
Разберём <P extends object>
<P>— это обобщённый (generic) тип, который позволяет передавать тип пропсов WrappedComponent.extends objectограничивает, чтоPдолжен быть объектом. Это защитит вас от передачи, например, строки или числа вместо пропсов.
Теперь HOC принимает только те пропсы, которые передаются в WrappedComponent.
Пример использования:
const MyComponent: React.FC<WrappedComponentProps> = ({ title }) => {
return <h1>{title}</h1>;
};
// Оборачиваем HOC
const EnhancedComponent = withLoading(MyComponent);
// Используем обёрнутый компонент
<EnhancedComponent title="Hello, HOC!" />;
Как добавить пропсы в HOC?
Теперь добавим собственные пропсы в HOC. Например, мы хотим передать проп message для настройки текста при загрузке. Для этого HOC должен принимать дополнительные пропсы.
Шаг 1: Создаём интерфейс для HOC-пропсов
interface WithLoadingProps {
message?: string;
}
Шаг 2: Расширяем типы пропсов HOC
Теперь мы можем объединить WithLoadingProps с P:
const withLoading = <P extends object>(
Component: React.ComponentType<P>
) => {
return ({ message, ...props }: WithLoadingProps & P) => {
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
setTimeout(() => setIsLoading(false), 3000);
}, []);
return isLoading ? <p>{message || 'Loading...'}</p> : <Component {...(props as P)} />;
};
};
Всё вместе
const MyComponent: React.FC<WrappedComponentProps> = ({ title }) => {
return <h1>{title}</h1>;
};
// Оборачиваем компонент с новым пропом
const EnhancedComponent = withLoading(MyComponent);
// Используем HOC с дополнительным пропсом
<EnhancedComponent title="Hello, TypeScript!" message="Please wait, data is loading..." />;
Углубляемся в типизацию: React.ComponentType
Вы, наверное, заметили, что мы используем React.ComponentType<P>. Это универсальный тип в React, который покрывает как функциональные, так и классовые компоненты. Если вы попытаетесь использовать вместо него React.FC<P>, вы получите ошибки при оборачивании классовых компонентов.
Сравните:
import React from 'react';
// Используем React.FC — будет работать только с функциональными компонентами
const withLoadingFC = <P extends object>(Component: React.FC<P>) => {
return (props: P) => <Component {...props} />;
};
// Используем React.ComponentType — универсальный подход
const withLoadingCT = <P extends object>(Component: React.ComponentType<P>) => {
return (props: P) => <Component {...props} />;
};
Типизация сложных HOC с помощью интерфейсов
Когда HOC становится более сложным, использование интерфейсов для типизации пропсов становится особенно важным. Например, давайте рассмотрим HOC для проверки аутентификации.
Шаг 1: Интерфейс для HOC-пропсов
Создадим интерфейс, который определяет, что компонент может получать проп isAuthenticated:
interface WithAuthProps {
isAuthenticated: boolean;
}
Шаг 2: Реализация HOC
const withAuth = <P extends object>(
Component: React.ComponentType<P>
) => {
return (props: WithAuthProps & P) => {
if (!props.isAuthenticated) {
return <p>You must be logged in to view this content.</p>;
}
return <Component {...(props as P)} />;
};
};
Пример использования:
const Dashboard: React.FC = () => {
return <h1>Welcome to the Dashboard!</h1>;
};
const ProtectedDashboard = withAuth(Dashboard);
// Используем HOC
<ProtectedDashboard isAuthenticated={true} />;
<ProtectedDashboard isAuthenticated={false} />;
Типизация HOC, обрабатывающего разные типы пропсов
Иногда HOC должен работать с различными пропсами. Например, компонент может получать необязательные пропсы, а HOC добавляет обязательные.
Рассмотрим HOC, который добавляет обязательный проп timestamp:
interface WithTimestampProps {
timestamp: number;
}
const withTimestamp = <P extends object>(
Component: React.ComponentType<P & WithTimestampProps>
) => {
return (props: P) => {
const timestamp = Date.now();
return <Component {...props} timestamp={timestamp} />;
};
};
// Используем HOC
const MyComponent: React.FC<{ name: string } & WithTimestampProps> = ({ name, timestamp }) => {
return (
<div>
<p>Name: {name}</p>
<p>Timestamp: {timestamp}</p>
</div>
);
};
const EnhancedComponent = withTimestamp(MyComponent);
<EnhancedComponent name="John Doe" />;
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ