通用 UseForm 钩子

Generic UseForm Hook

我正在尝试使用泛型和 Typescript 创建一个 useForm 挂钩。 我希望消费者能够输入一个界面和初始状态,然后让挂钩动态创建一个带有添加验证字段的新状态。

例如用户传入:

const {formState, handleSubmit, updateField} = useForm<{
        email: string;
    }>({
        email: "",
    });

在幕后,钩子会创建这样的状态:

{
  email: {
    value: "",
    validationMessage: "",
    validationType: "none" // as opposed to "success" or "error"
  }
}

我正在尝试通过 reduce 实现这一点,代码如下:

interface FormField {
    value: string,
    validationType: ValidationProperty,
    validationMessage: string 
}

function isInInitialState<T>(val: string | keyof T, vals: T): val is keyof T {
  return typeof val === "string" && Object.keys(vals).includes(val)
}

function useForm<T>(initialFormState: T) {

  const [formState, setFormState] = useState<Record<keyof T, FormField>>(Object.keys(initialFormState).reduce<Record<keyof T, FormField>>((previous, current) => {
    return {
      ...previous,
      [current]: {
        value: isInInitialState(current, initialFormState) ? previous[current] : "",
        validationType: "none",
        validationMessage: ""
      }
    }
  }, {}));

...rest of hook.

Typescript 的问题是我的空对象的起始值与 T 的形状不同,因此编译器会抛出错误。同样,如果我使用与 initialFormState 相同的值填充起始值,我也会遇到同样的问题。

简而言之,我如何告诉 Typescript 我肯定会以 Record<keyof T, FormField>> 结束,所以它不需要担心起始值?

我认为您可以使用类型断言来解决您的问题。

有时 typescript 无法正确推断类型,当您调用 reduce 函数时就会发生这种情况。因此,您可以告诉打字稿您的常数的预期 return 是什么。


type ValidationProperty = 'none' | 'other'

interface FormField {
  value: string
  validationType: ValidationProperty
  validationMessage: string
}

type FormState<T> = Record<keyof T, FormField>

function useForm<T>(initialFormState: T) {
  const keys = Object.keys(initialFormState)
  const defaultFormState = keys.reduce(
    (acc, key) => ({
      ...acc,
      [key]: {
        value: initialFormState[key],
        validationType: 'none',
        validationMessage: ''
      }
    }),
    {}
  ) as FormState<T> // Type assertion here

  const [formState, setFormState] = useState<FormState<T>>(defaultFormState)

  return [formState]
}

const MyComponent = () => {
  const [form] = useForm({ email: 'string' })
  // form has the type FormState<{email: string}>
  return <div>Hello</div>
}

使用 type assertion will be helpful in your case. The reason this will be necessary is that the return type of Object.keys() is always string[] in TypeScript. This is because TS is structurally-typed,因此对象可以具有超出您在类型中定义的属性的额外属性。

在这种情况下提取转换函数也很有用:

TS Playground

import {useState} from 'react';

type ValidationType = 'error' | 'none' | 'success';

type FormField = {
  validationMessage: string;
  validationType: ValidationType;
  value: string;
};

function transformObject <T extends Record<string, string>>(o: T): Record<keyof T, FormField> {
  return Object.keys(o).reduce((previous, key) => ({
    ...previous,
    [key]: {
      value: o[key as keyof T],
      validationType: 'none',
      validationMessage: '',
    }
  }), {} as Record<keyof T, FormField>);
}

function useForm <T extends Record<string, string>>(initialFormState: T) {
  const [formState, setFormState] = useState(transformObject(initialFormState));
  // rest of hook...
}

transformObject({email: 999}); // Desirable compiler error
//               ~~~~~
// Type 'number' is not assignable to type 'string'.(2322)

const transformed = transformObject({email: 'recipient@name.tld'}); // ok

console.log(transformed); // Result:
// {
//   "email": {
//     "value": "recipient@name.tld",
//     "validationType": "none",
//     "validationMessage": ""
//   }
// }