Другое

Исправление ошибки 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:

tsx
"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 следующим образом:

tsx
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. useContextSelector из use-context-selector по умолчанию возвращает unknown
  2. Приведение типа (ctx as FormContext<TForm>) не помогает TypeScript понять конкретный тип
  3. TypeScript не может гарантировать, что ctx.form имеет свойства, к которым вы пытаетесь получить доступ

Когда вы пишете:

typescript
const { watch } = useFormSelector<HomeworkSchemaType>(...);

TypeScript видит, что ctx.form имеет тип unknown, поэтому не может получить доступ к свойству watch.


Решение 1: Улучшенный вывод типов

Измените хук useFormSelector, чтобы TypeScript мог выводить возвращаемый тип:

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

Однако это все еще требует второго обобщенного аргумента. Вот лучший подход:

typescript
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: Использование перегрузок функций

Для лучшего вывода типов используйте перегрузки функций:

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

Это позволяет использовать его с или без функции селектора:

typescript
// Без селектора - возвращает всю форму
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: Типизация значения по умолчанию контекста

Улучшите типизацию значения по умолчанию контекста:

typescript
export const FormContext = createContext<FormContext<FieldValues> | null>(null);

Затем обновите провайдер формы:

typescript
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:

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

Полная рабочая реализация

Вот полное решение, которое работает без необходимости второго обобщенного аргумента:

typescript
"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;
}

Теперь вы можете использовать его следующим образом:

typescript
const { control, errors, watch } = useFormSelector<HomeworkSchemaType>((ctx) => ({
  control: ctx.form.control,
  errors: ctx.form.formState.errors,
  watch: ctx.form.watch,
}));

Альтернативные подходы

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

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

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

Использование шаблона обобщенного селектора

typescript
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;

Лучшие практики

  1. Всегда предоставляйте проверки на null при использовании use-context-selector со значениями контекста
  2. Используйте правильную обработку ошибок для случаев, когда контекст не предоставлен
  3. Рассмотрите возможность использования значений по умолчанию для распространенных операций с формами
  4. Четко документируйте ваши хуки, чтобы направлять пользователей на правильное использование
  5. Тестируйте крайние случаи, такие как вложенные контексты и сложные селекторы

Ключевое понимание заключается в том, что TypeScript требует явной типизации для разрешения типа unknown из useContextSelector. Предоставляя правильные ограничения и используя вывод типов, вы можете создать типобезопасное решение, которое работает без необходимости ручных аннотаций типов для возвращаемого значения.


Источники

  1. Документация use-context-selector
  2. Документация React Hook Form TypeScript
  3. Документация TypeScript Generics
  4. Лучшие практики React Context
Авторы
Проверено модерацией
Модерация