Home > Net >  How to specify return type of a reusable useForm hook?
How to specify return type of a reusable useForm hook?

Time:07-30

I'm trying to make a reusable useForm hook to use it on two kinds of forms. First kind is a form with just inputs so its type is:

type Input = {
  [key: string]: string;
}

Second kind is a form with inputs and selects so its type something like this:

type Input = {
  [key: string]: string | { [key: string]: string };
};

Hook accepts initial value. For example for form with inputs and selects I will provide this init value:

const INIT_INPUT = {
  name: '',
  description: '',
  category: {
    id: '',
    name: '',
  },
};

How can I specify return type from hook? I'm sorry it's difficult to explain what I need. I will try on example:

Hook

type Input = {
  [key: string]: string | { [key: string]: string };
};

const useForm = (initInput: Input, callback: () => void) => {
  const [input, setInput] = useState(initInput);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
    const { name, value } = e.target;

    setInput((prevState: any) => ({
      ...prevState,
      [name]: value,
    }));
  };

  const handleChangeSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const { value, name } = e.target;
    const id = e.target.selectedOptions[0].getAttribute('data-set');

    setInput((prevState: any) => ({
      ...prevState,
      [name]: {
        name: value,
        id,
      },
    }));
  };

  const submit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    callback();
  };

  return { input, setInput, handleChange, submit, handleChangeSelect };
};

export default useForm;

Component with form:

On each input I get an error because TS doesn't know if input.name is a string or a object. How to fix that without as string on each input?

TS error

Type 'string | { [key: string]: string; }' is not assignable to type 'string'. Type '{ [key: string]: string; }' is not assignable to type 'string'.

const INIT_INPUT = {
  name: '',
  description: '',
  price: '',
  image: '',
  weight: '',
  brand: '',
  category: {
    id: '',
    name: '',
  },
};

const SomeForm: React.FC = () => {
  const { input, setInput, handleChange, submit, handleChangeSelect } = useForm(INIT_INPUT, submitHandler);

  return (
    <>
      <input value={input.name || ''} onChange={handleChange} />
      <input value={input.description || ''} onChange={handleChange} />
      //...other inputs
    </>
  );
};

CodePudding user response:

This is actually a question about TypeScript than purely React, but let's go.

You can solve this by using generics. So, you'll want to add a type parameter to the hook that extends an object ({} or Record<string, Something>) and then use that same type as the return type for the hook. For example:

const useForm = <T extends {}>(initInput: T, callback: () => void): 
 { 
    input: T,  
    // the other output types
 } => {
  // rest of your hook
}

Also, for the typing of the input data to be more precise, mark it as as const:

const INIT_INPUT = {
  name: '',
  description: '',
  price: '',
  image: '',
  weight: '',
  brand: '',
  category: {
    id: '',
    name: '',
  },
} as const;

So that way, the returned type will be inferred to be the same as the input's type, and you'll know the type of each individual property.

Finally, some React tips is to wrap each function inside the hook in a useCallback to avoid unnecessary re-renders, as well as the submitHandler that is an argument to the hook.

  • Related