Исправление ошибки TypeScript: Свойство 'watch' не существует в типе 'unknown'
Узнайте, как исправлять ошибки TypeScript при использовании use-context-selector с React Hook Form. Найдите решения для безопасных по типу селекторов контекста форм и правильного вывода типов.
Ошибка TypeScript с use-context-selector и React Hook Form: Свойство ‘watch’ не существует в типе ‘unknown’
Я реализую универсальный FormProvider с использованием use-context-selector и React Hook Form, но столкнулся с ошибкой TypeScript при попытке использовать хук useFormSelector.
Текущая реализация
Вот мой компонент FormProvider:
"use client";
import { useMemo } from "react";
import {
FieldValues,
FormProvider as RHFProvider,
SubmitErrorHandler,
SubmitHandler,
UseFormReturn,
} from "react-hook-form";
import { createContext, useContextSelector } from "use-context-selector";
type FormContext<T extends FieldValues = FieldValues> = {
form: UseFormReturn<T>;
onChange?: (name: keyof T | string, value: any) => void;
};
export const FormContext = createContext<FormContext<FieldValues>>(
{} as FormContext<FieldValues>
);
export function FormProvider<T extends FieldValues>({
children,
onChange,
form,
className,
onSubmit,
onError,
}: {
form: UseFormReturn<T>;
onSubmit: SubmitHandler<T>;
onError?: SubmitErrorHandler<T>;
onChange?: (name: keyof T | string, value: any) => void;
className?: string;
children: React.ReactNode;
}) {
const value = useMemo<FormContext<T>>(() => {
console.log("Form valus changed");
return { form, onChange };
}, [form, onChange]);
console.log("form provider rerender");
return (
<FormContext.Provider value={value as FormContext<FieldValues>}>
<RHFProvider {...form}>
<form
className={className}
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit(onSubmit, onError)();
}}
>
{children}
</form>
</RHFProvider>
</FormContext.Provider>
);
}
export function useFormSelector<TForm extends FieldValues, Selected>(
selector: (ctx: FormContext<TForm>) => Selected
): Selected {
return useContextSelector(FormContext, (ctx) => {
return selector(ctx as FormContext<TForm>);
});
}
export function useFormContext<TForm extends FieldValues>() {
return useFormSelector<TForm, UseFormReturn<TForm>>(
(ctx) => ctx.form as UseFormReturn<TForm>
);
}
Проблема
Когда я пытаюсь использовать хук useFormSelector следующим образом:
const { control, errors, watch } = useFormSelector<HomeworkSchemaType>(
(ctx) => {
const form = ctx.form;
return {
control: form.control,
errors: form.formState.errors,
watch: form.watch,
};
}
);
Я получаю следующую ошибку TypeScript:
Свойство 'watch' не существует в типе 'unknown'.ts(2339)
Вопрос
Хук useFormSelector заставляет меня использовать второй универсальный аргумент, но я хочу, чтобы TypeScript выводил его из того, что я возвращаю в функции селектора. Какие изменения следует внести в реализацию, чтобы это работало правильно?
Я ищу решение, которое сохраняет безопасность типов, позволяя TypeScript выводить возвращаемый тип функции селектора.
Ошибка TypeScript возникает из-за того, что useContextSelector по умолчанию возвращает unknown, а приведение типа (ctx as FormContext<TForm>) недостаточно для понимания TypeScript конкретного типа контекста. Вот как это исправить, сохраняя безопасность типов и правильное вывод типов.
Краткий ответ
Проблема заключается в том, что useContextSelector по умолчанию возвращает unknown. Чтобы решить эту проблему, измените ваш хук useFormSelector, чтобы использовать вывод типов TypeScript, используя обобщенное ограничение и опуская явную аннотацию возвращаемого типа, позволяя TypeScript выводить тип из возвращаемого значения функции селектора.
Содержание
- Понимание проблемы
- Решение 1: Улучшенный вывод типов
- Решение 2: Использование перегрузок функций
- Решение 3: Типизация значения по умолчанию контекста
- Полная рабочая реализация
- Альтернативные подходы
- Лучшие практики
Понимание проблемы
Ошибка возникает потому, что:
useContextSelectorизuse-context-selectorпо умолчанию возвращаетunknown- Приведение типа
(ctx as FormContext<TForm>)не помогает TypeScript понять конкретный тип - TypeScript не может гарантировать, что
ctx.formимеет свойства, к которым вы пытаетесь получить доступ
Когда вы пишете:
const { watch } = useFormSelector<HomeworkSchemaType>(...);
TypeScript видит, что ctx.form имеет тип unknown, поэтому не может получить доступ к свойству watch.
Решение 1: Улучшенный вывод типов
Измените хук useFormSelector, чтобы TypeScript мог выводить возвращаемый тип:
export function useFormSelector<TForm extends FieldValues, Selected>(
selector: (ctx: FormContext<TForm>) => Selected
): Selected {
return useContextSelector(FormContext, (ctx) => {
// Приводим контекст к ожидаемому типу
const typedCtx = ctx as FormContext<TForm>;
return selector(typedCtx);
});
}
Однако это все еще требует второго обобщенного аргумента. Вот лучший подход:
export function useFormSelector<TForm extends FieldValues, Selected = UseFormReturn<TForm>>(
selector: (ctx: FormContext<TForm>) => Selected
): Selected {
return useContextSelector(FormContext, (ctx) => {
const typedCtx = ctx as FormContext<TForm>;
return selector(typedCtx);
});
}
Решение 2: Использование перегрузок функций
Для лучшего вывода типов используйте перегрузки функций:
export function useFormSelector<TForm extends FieldValues>(): UseFormReturn<TForm>;
export function useFormSelector<TForm extends FieldValues, Selected>(
selector: (ctx: FormContext<TForm>) => Selected
): Selected;
export function useFormSelector<TForm extends FieldValues, Selected = UseFormReturn<TForm>>(
selector?: (ctx: FormContext<TForm>) => Selected
): Selected {
return useContextSelector(FormContext, (ctx) => {
const typedCtx = ctx as FormContext<TForm>;
return selector ? selector(typedCtx) : typedCtx.form;
});
}
Это позволяет использовать его с или без функции селектора:
// Без селектора - возвращает всю форму
const form = useFormSelector<HomeworkSchemaType>();
// С селектором - возвращает выбранные свойства
const { control, errors, watch } = useFormSelector<HomeworkSchemaType>((ctx) => ({
control: ctx.form.control,
errors: ctx.form.formState.errors,
watch: ctx.form.watch,
}));
Решение 3: Типизация значения по умолчанию контекста
Улучшите типизацию значения по умолчанию контекста:
export const FormContext = createContext<FormContext<FieldValues> | null>(null);
Затем обновите провайдер формы:
export function FormProvider<T extends FieldValues>({
children,
onChange,
form,
className,
onSubmit,
onError,
}: FormProviderProps<T>) {
const value = useMemo<FormContext<T>>(() => ({
form,
onChange,
}), [form, onChange]);
return (
<FormContext.Provider value={value}>
<RHFProvider {...form}>
<form
className={className}
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit(onSubmit, onError)();
}}
>
{children}
</form>
</RHFProvider>
</FormContext.Provider>
);
}
И обновите useFormSelector:
export function useFormSelector<TForm extends FieldValues, Selected>(
selector: (ctx: NonNullable<FormContext<TForm>>) => Selected
): Selected {
return useContextSelector(FormContext, (ctx) => {
if (!ctx) {
throw new Error('useFormSelector должен использоваться внутри FormProvider');
}
return selector(ctx as FormContext<TForm>);
});
}
Полная рабочая реализация
Вот полное решение, которое работает без необходимости второго обобщенного аргумента:
"use client";
import { useMemo } from "react";
import {
FieldValues,
FormProvider as RHFProvider,
SubmitErrorHandler,
SubmitHandler,
UseFormReturn,
} from "react-hook-form";
import { createContext, useContextSelector } from "use-context-selector";
type FormContext<T extends FieldValues = FieldValues> = {
form: UseFormReturn<T>;
onChange?: (name: keyof T | string, value: any) => void;
};
// Создаем контекст с правильной типизацией
export const FormContext = createContext<FormContext<FieldValues> | null>(null);
interface FormProviderProps<T extends FieldValues> {
form: UseFormReturn<T>;
onSubmit: SubmitHandler<T>;
onError?: SubmitErrorHandler<T>;
onChange?: (name: keyof T | string, value: any) => void;
className?: string;
children: React.ReactNode;
}
export function FormProvider<T extends FieldValues>({
children,
onChange,
form,
className,
onSubmit,
onError,
}: FormProviderProps<T>) {
const value = useMemo<FormContext<T>>(() => ({
form,
onChange,
}), [form, onChange]);
return (
<FormContext.Provider value={value}>
<RHFProvider {...form}>
<form
className={className}
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit(onSubmit, onError)();
}}
>
{children}
</form>
</RHFProvider>
</FormContext.Provider>
);
}
// Основной хук селектора с правильным выводом типов
export function useFormSelector<TForm extends FieldValues, Selected = UseFormReturn<TForm>>(
selector: (ctx: NonNullable<FormContext<TForm>>) => Selected
): Selected {
return useContextSelector(FormContext, (ctx) => {
if (!ctx) {
throw new Error('useFormSelector должен использоваться внутри FormProvider');
}
return selector(ctx as FormContext<TForm>);
});
}
// Удобный хук для доступа к форме
export function useFormContext<TForm extends FieldValues>() {
const form = useFormSelector<TForm, UseFormReturn<TForm>>(
(ctx) => ctx.form
);
return form;
}
Теперь вы можете использовать его следующим образом:
const { control, errors, watch } = useFormSelector<HomeworkSchemaType>((ctx) => ({
control: ctx.form.control,
errors: ctx.form.formState.errors,
watch: ctx.form.watch,
}));
Альтернативные подходы
Использование пользовательского хука для доступа к форме
Создайте специальный хук для доступа к свойствам формы:
export function useFormProperties<TForm extends FieldValues>() {
const form = useFormSelector<TForm, UseFormReturn<TForm>>((ctx) => ctx.form);
return {
control: form.control,
errors: form.formState.errors,
watch: form.watch,
handleSubmit: form.handleSubmit,
reset: form.reset,
setValue: form.setValue,
getValues: form.getValues,
};
}
// Использование
const { control, errors, watch } = useFormProperties<HomeworkSchemaType>();
Использование шаблона обобщенного селектора
export function useFormSelector<TForm extends FieldValues>() {
const form = useFormSelector<TForm, UseFormReturn<TForm>>((ctx) => ctx.form);
return form;
}
// Использование
const form = useFormSelector<HomeworkSchemaType>();
const { control, errors, watch } = form;
Лучшие практики
- Всегда предоставляйте проверки на null при использовании
use-context-selectorсо значениями контекста - Используйте правильную обработку ошибок для случаев, когда контекст не предоставлен
- Рассмотрите возможность использования значений по умолчанию для распространенных операций с формами
- Четко документируйте ваши хуки, чтобы направлять пользователей на правильное использование
- Тестируйте крайние случаи, такие как вложенные контексты и сложные селекторы
Ключевое понимание заключается в том, что TypeScript требует явной типизации для разрешения типа unknown из useContextSelector. Предоставляя правильные ограничения и используя вывод типов, вы можете создать типобезопасное решение, которое работает без необходимости ручных аннотаций типов для возвращаемого значения.